@beunec/rivine 0.1.0
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 +423 -0
- package/action.yml +165 -0
- package/dist/api/workspace.js +152 -0
- package/dist/auth/device.js +73 -0
- package/dist/auth/keychain.js +88 -0
- package/dist/auth/pkce.js +142 -0
- package/dist/auth/session.js +101 -0
- package/dist/config.js +37 -0
- package/dist/index.js +300 -0
- package/dist/mcp/config.js +73 -0
- package/dist/mcp/manager.js +154 -0
- package/dist/mcp/oauth.js +234 -0
- package/dist/mcp/relevance.js +74 -0
- package/dist/protocol/edit.js +247 -0
- package/dist/protocol/fsExecutor.js +471 -0
- package/dist/protocol/git.js +157 -0
- package/dist/protocol/permissions.js +210 -0
- package/dist/protocol/runner.js +325 -0
- package/dist/protocol/sessionKey.js +57 -0
- package/dist/protocol/shell.js +260 -0
- package/dist/protocol/snapshot.js +84 -0
- package/dist/protocol/stream.js +41 -0
- package/dist/runHeadless.js +140 -0
- package/dist/session.js +57 -0
- package/dist/ui/App.js +444 -0
- package/dist/ui/components.js +92 -0
- package/dist/ui/headless.js +130 -0
- package/dist/ui/render.js +109 -0
- package/dist/ui/slashCommands.js +57 -0
- package/dist/ui/theme.js +82 -0
- package/package.json +45 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Rivine Code CLI
|
|
3
|
+
* Licensed and owned by Beunec Technologies, Inc.
|
|
4
|
+
*
|
|
5
|
+
* https://aselius.beunec.com
|
|
6
|
+
*
|
|
7
|
+
* This software is proprietary. Use is governed by the licence agreement
|
|
8
|
+
* accompanying it; it is not open source and is not licensed for
|
|
9
|
+
* redistribution or derivative works.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { MEDIATOR_MODE, TARGET_ENGINE, apiUrl } from '../config.js';
|
|
13
|
+
import { accessToken } from '../auth/session.js';
|
|
14
|
+
export class InsufficientCredits extends Error {
|
|
15
|
+
detail;
|
|
16
|
+
constructor(detail) {
|
|
17
|
+
super(detail);
|
|
18
|
+
this.detail = detail;
|
|
19
|
+
this.name = 'InsufficientCredits';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function authHeaders() {
|
|
23
|
+
return {
|
|
24
|
+
authorization: `Bearer ${await accessToken()}`,
|
|
25
|
+
'content-type': 'application/json',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function projectKey(cwd) {
|
|
29
|
+
return createHash('sha256').update(cwd).digest('hex').slice(0, 40);
|
|
30
|
+
}
|
|
31
|
+
export async function bootstrap(cwd, label) {
|
|
32
|
+
const url = new URL(`${apiUrl()}/api/cli/bootstrap`);
|
|
33
|
+
url.searchParams.set('project_key', projectKey(cwd));
|
|
34
|
+
url.searchParams.set('label', label);
|
|
35
|
+
const res = await fetch(url, { headers: await authHeaders() });
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const detail = await res
|
|
38
|
+
.json()
|
|
39
|
+
.then((b) => String(b?.error ?? b?.detail ?? '').trim())
|
|
40
|
+
.catch(() => '');
|
|
41
|
+
if (res.status === 401) {
|
|
42
|
+
throw new Error('Your session is no longer valid. Run `rivine login`.');
|
|
43
|
+
}
|
|
44
|
+
if (res.status === 404) {
|
|
45
|
+
const deployed = await fetch(`${apiUrl()}/api/cli/config`)
|
|
46
|
+
.then((r) => r.ok)
|
|
47
|
+
.catch(() => false);
|
|
48
|
+
if (!deployed) {
|
|
49
|
+
throw new Error(`${apiUrl()} does not serve the terminal API — /api/cli/config is not ` +
|
|
50
|
+
'there either. This build of the Workspace has not been deployed with ' +
|
|
51
|
+
'Rivine Code CLI support, or RIVINE_API_URL points somewhere else.');
|
|
52
|
+
}
|
|
53
|
+
throw new Error('That account has no Aselius profile. Sign in with the account you use ' +
|
|
54
|
+
'on aselius.beunec.com — `rivine logout` then `rivine login`.');
|
|
55
|
+
}
|
|
56
|
+
if (detail.toLowerCase().includes('authentication')) {
|
|
57
|
+
throw new Error(`The server could not look up your account (${detail}). ` +
|
|
58
|
+
'Its identity service is unreachable — if this is a local or staging ' +
|
|
59
|
+
'Workspace, check that the Node backend is running and its cloud ' +
|
|
60
|
+
'credentials are valid.');
|
|
61
|
+
}
|
|
62
|
+
throw new Error(detail
|
|
63
|
+
? `Could not start a session (HTTP ${res.status}): ${detail}`
|
|
64
|
+
: `Could not start a session (HTTP ${res.status}).`);
|
|
65
|
+
}
|
|
66
|
+
return (await res.json());
|
|
67
|
+
}
|
|
68
|
+
export async function openTurn(req) {
|
|
69
|
+
const res = await fetch(`${apiUrl()}/api/workspaces/${req.workspaceId}/chats/${req.chatSpaceId}/stream`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { ...(await authHeaders()), accept: 'text/event-stream' },
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
message: req.message,
|
|
74
|
+
mediator_mode: MEDIATOR_MODE,
|
|
75
|
+
target_engine: TARGET_ENGINE,
|
|
76
|
+
client: 'cli',
|
|
77
|
+
cli_session_id: req.sessionId,
|
|
78
|
+
permission_mode: req.permissionMode,
|
|
79
|
+
cwd_snapshot: req.cwdSnapshot ?? null,
|
|
80
|
+
mcp_tools: req.mcpTools ?? null,
|
|
81
|
+
cli_session_key: req.sessionKey ?? null,
|
|
82
|
+
sources: [],
|
|
83
|
+
tools: [],
|
|
84
|
+
connector_ids: [],
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
if (res.status === 402) {
|
|
88
|
+
const body = (await res.json().catch(() => ({})));
|
|
89
|
+
throw new InsufficientCredits(body.error ?? 'Not enough credits for Rivine Code.');
|
|
90
|
+
}
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const body = await res.text().catch(() => '');
|
|
93
|
+
throw new Error(`The engine refused this turn (HTTP ${res.status}). ${body.slice(0, 300)}`);
|
|
94
|
+
}
|
|
95
|
+
return res;
|
|
96
|
+
}
|
|
97
|
+
export async function postFsResult(sessionId, requestId, outcome, key) {
|
|
98
|
+
const data = outcome.ok ? (key ? key.seal(outcome.data) : outcome.data) : null;
|
|
99
|
+
await fetch(`${apiUrl()}/api/cli/sessions/${sessionId}/fs-result`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: await authHeaders(),
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
request_id: requestId,
|
|
104
|
+
ok: outcome.ok,
|
|
105
|
+
data,
|
|
106
|
+
error: outcome.ok ? null : outcome.error,
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export async function resumeTurn(args) {
|
|
111
|
+
const res = await fetch(`${apiUrl()}/api/workspaces/${args.workspaceId}/chats/${args.chatSpaceId}/resume`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: await authHeaders(),
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
thread_id: args.threadId,
|
|
116
|
+
approved: args.approved,
|
|
117
|
+
decision: args.approved ? 'approved' : 'rejected',
|
|
118
|
+
feedback: args.feedback ?? '',
|
|
119
|
+
modified_args: args.modifiedArgs ?? null,
|
|
120
|
+
cli_session_key: args.sessionKey ?? null,
|
|
121
|
+
cli_session_id: args.sessionId,
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
if (!res.ok) {
|
|
125
|
+
throw new Error(`Could not resume the turn (HTTP ${res.status}).`);
|
|
126
|
+
}
|
|
127
|
+
return (await res.json());
|
|
128
|
+
}
|
|
129
|
+
export async function closeSession(sessionId) {
|
|
130
|
+
try {
|
|
131
|
+
await fetch(`${apiUrl()}/api/cli/sessions/${sessionId}/close`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: await authHeaders(),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function fetchCredits(creditsUrl) {
|
|
140
|
+
if (!creditsUrl)
|
|
141
|
+
return null;
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch(creditsUrl, { headers: await authHeaders() });
|
|
144
|
+
if (!res.ok)
|
|
145
|
+
return null;
|
|
146
|
+
const body = (await res.json());
|
|
147
|
+
return { balance: Number(body.balance ?? 0), plan: String(body.plan ?? 'free') };
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Rivine Code CLI
|
|
3
|
+
* Licensed and owned by Beunec Technologies, Inc.
|
|
4
|
+
*
|
|
5
|
+
* https://aselius.beunec.com
|
|
6
|
+
*
|
|
7
|
+
* This software is proprietary. Use is governed by the licence agreement
|
|
8
|
+
* accompanying it; it is not open source and is not licensed for
|
|
9
|
+
* redistribution or derivative works.
|
|
10
|
+
*/
|
|
11
|
+
export async function startDeviceFlow(config) {
|
|
12
|
+
const res = await fetch(`https://${config.auth0_domain}/oauth/device/code`, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
15
|
+
body: new URLSearchParams({
|
|
16
|
+
client_id: config.auth0_client_id,
|
|
17
|
+
scope: config.scope,
|
|
18
|
+
audience: config.auth0_audience,
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
throw new Error(`Could not start device sign-in (HTTP ${res.status}). ` +
|
|
23
|
+
`Device code may not be enabled for this application.`);
|
|
24
|
+
}
|
|
25
|
+
const body = (await res.json());
|
|
26
|
+
let intervalMs = (body.interval ?? 5) * 1000;
|
|
27
|
+
const deadline = Date.now() + body.expires_in * 1000;
|
|
28
|
+
const poll = async () => {
|
|
29
|
+
for (;;) {
|
|
30
|
+
if (Date.now() > deadline) {
|
|
31
|
+
throw new Error('The code expired before it was approved. Run rivine login --device again.');
|
|
32
|
+
}
|
|
33
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
34
|
+
const tokenRes = await fetch(`https://${config.auth0_domain}/oauth/token`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
37
|
+
body: new URLSearchParams({
|
|
38
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
39
|
+
device_code: body.device_code,
|
|
40
|
+
client_id: config.auth0_client_id,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
const payload = (await tokenRes.json().catch(() => ({})));
|
|
44
|
+
if (tokenRes.ok && payload.access_token) {
|
|
45
|
+
return {
|
|
46
|
+
accessToken: payload.access_token,
|
|
47
|
+
refreshToken: payload.refresh_token,
|
|
48
|
+
expiresAt: Date.now() + (payload.expires_in ?? 3600) * 1000,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (payload.error === 'authorization_pending')
|
|
52
|
+
continue;
|
|
53
|
+
if (payload.error === 'slow_down') {
|
|
54
|
+
intervalMs += 5000;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
throw new Error(payload.error === 'expired_token'
|
|
58
|
+
? 'The code expired before it was approved.'
|
|
59
|
+
: payload.error === 'access_denied'
|
|
60
|
+
? 'Sign-in was declined.'
|
|
61
|
+
: `Device sign-in failed: ${payload.error_description || payload.error || tokenRes.status}`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
start: {
|
|
66
|
+
userCode: body.user_code,
|
|
67
|
+
verificationUri: body.verification_uri,
|
|
68
|
+
verificationUriComplete: body.verification_uri_complete,
|
|
69
|
+
expiresIn: body.expires_in,
|
|
70
|
+
},
|
|
71
|
+
poll,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Rivine Code CLI
|
|
3
|
+
* Licensed and owned by Beunec Technologies, Inc.
|
|
4
|
+
*
|
|
5
|
+
* https://aselius.beunec.com
|
|
6
|
+
*
|
|
7
|
+
* This software is proprietary. Use is governed by the licence agreement
|
|
8
|
+
* accompanying it; it is not open source and is not licensed for
|
|
9
|
+
* redistribution or derivative works.
|
|
10
|
+
*/
|
|
11
|
+
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
const SERVICE = 'beunec-rivine-cli';
|
|
15
|
+
const ACCOUNT = 'default';
|
|
16
|
+
const filePath = (account = ACCOUNT) => account === ACCOUNT
|
|
17
|
+
? join(homedir(), '.rivine', 'credentials.json')
|
|
18
|
+
: join(homedir(), '.rivine', 'credentials', `${encodeURIComponent(account)}.json`);
|
|
19
|
+
const keyrings = new Map();
|
|
20
|
+
async function getKeyring(account = ACCOUNT) {
|
|
21
|
+
const cached = keyrings.get(account);
|
|
22
|
+
if (cached !== undefined)
|
|
23
|
+
return cached;
|
|
24
|
+
if (process.env.RIVINE_NO_KEYCHAIN) {
|
|
25
|
+
keyrings.set(account, null);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
let entry = null;
|
|
29
|
+
try {
|
|
30
|
+
const mod = await import('@napi-rs/keyring');
|
|
31
|
+
const candidate = new mod.Entry(SERVICE, account);
|
|
32
|
+
candidate.getPassword();
|
|
33
|
+
entry = candidate;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
entry = null;
|
|
37
|
+
}
|
|
38
|
+
keyrings.set(account, entry);
|
|
39
|
+
return entry;
|
|
40
|
+
}
|
|
41
|
+
export async function usingKeychain() {
|
|
42
|
+
return (await getKeyring()) !== null;
|
|
43
|
+
}
|
|
44
|
+
export function mcpAccount(serverName) {
|
|
45
|
+
return `mcp:${serverName}`;
|
|
46
|
+
}
|
|
47
|
+
export async function saveSession(session, account = ACCOUNT) {
|
|
48
|
+
const payload = JSON.stringify(session);
|
|
49
|
+
const ring = await getKeyring(account);
|
|
50
|
+
if (ring) {
|
|
51
|
+
ring.setPassword(payload);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const path = filePath(account);
|
|
55
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
56
|
+
await writeFile(path, payload, { encoding: 'utf8', mode: 0o600 });
|
|
57
|
+
await chmod(path, 0o600);
|
|
58
|
+
}
|
|
59
|
+
export async function loadSession(account = ACCOUNT) {
|
|
60
|
+
const ring = await getKeyring(account);
|
|
61
|
+
let raw = null;
|
|
62
|
+
if (ring) {
|
|
63
|
+
raw = ring.getPassword();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
raw = await readFile(filePath(account), 'utf8').catch(() => null);
|
|
67
|
+
}
|
|
68
|
+
if (!raw)
|
|
69
|
+
return null;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(raw);
|
|
72
|
+
return parsed?.accessToken ? parsed : null;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function clearSession(account = ACCOUNT) {
|
|
79
|
+
const ring = await getKeyring(account);
|
|
80
|
+
if (ring) {
|
|
81
|
+
try {
|
|
82
|
+
ring.deletePassword();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await rm(filePath(account), { force: true });
|
|
88
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Rivine Code CLI
|
|
3
|
+
* Licensed and owned by Beunec Technologies, Inc.
|
|
4
|
+
*
|
|
5
|
+
* https://aselius.beunec.com
|
|
6
|
+
*
|
|
7
|
+
* This software is proprietary. Use is governed by the licence agreement
|
|
8
|
+
* accompanying it; it is not open source and is not licensed for
|
|
9
|
+
* redistribution or derivative works.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
12
|
+
import { createServer } from 'node:http';
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
const base64url = (buf) => buf.toString('base64url');
|
|
15
|
+
function makeVerifier() {
|
|
16
|
+
return base64url(randomBytes(32));
|
|
17
|
+
}
|
|
18
|
+
function challengeFor(verifier) {
|
|
19
|
+
return base64url(createHash('sha256').update(verifier).digest());
|
|
20
|
+
}
|
|
21
|
+
function sameState(a, b) {
|
|
22
|
+
const left = Buffer.from(a);
|
|
23
|
+
const right = Buffer.from(b);
|
|
24
|
+
if (left.length !== right.length)
|
|
25
|
+
return false;
|
|
26
|
+
return timingSafeEqual(left, right);
|
|
27
|
+
}
|
|
28
|
+
export function openBrowser(url) {
|
|
29
|
+
if (process.env.RIVINE_NO_BROWSER)
|
|
30
|
+
return;
|
|
31
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
32
|
+
try {
|
|
33
|
+
const child = spawn(cmd, [url], { stdio: 'ignore', detached: true, shell: process.platform === 'win32' });
|
|
34
|
+
child.unref();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const DONE_PAGE = (title, body) => `<!doctype html><meta charset="utf-8"><title>${title}</title>` +
|
|
40
|
+
`<body style="font-family:ui-sans-serif,system-ui,sans-serif;display:grid;place-items:center;height:100vh;margin:0;background:#0b0b0c;color:#e8e8ea">` +
|
|
41
|
+
`<div style="text-align:center;max-width:34rem;padding:2rem">` +
|
|
42
|
+
`<h1 style="font-size:1.25rem;font-weight:600;margin:0 0 .5rem">${title}</h1>` +
|
|
43
|
+
`<p style="margin:0;color:#a1a1aa;line-height:1.6">${body}</p></div></body>`;
|
|
44
|
+
export async function loginWithPkce(config, onUrl, timeoutMs = 5 * 60_000) {
|
|
45
|
+
const verifier = makeVerifier();
|
|
46
|
+
const challenge = challengeFor(verifier);
|
|
47
|
+
const state = base64url(randomBytes(16));
|
|
48
|
+
const port = config.loopback_port;
|
|
49
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
50
|
+
const received = new Promise((resolve, reject) => {
|
|
51
|
+
const server = createServer((req, res) => {
|
|
52
|
+
const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
|
|
53
|
+
if (url.pathname !== '/callback') {
|
|
54
|
+
res.writeHead(404).end();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const finish = (status, page, done) => {
|
|
58
|
+
res.writeHead(status, { 'content-type': 'text/html; charset=utf-8' }).end(page);
|
|
59
|
+
res.on('finish', () => {
|
|
60
|
+
server.close();
|
|
61
|
+
done();
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const error = url.searchParams.get('error');
|
|
65
|
+
if (error) {
|
|
66
|
+
const description = url.searchParams.get('error_description') || error;
|
|
67
|
+
finish(400, DONE_PAGE('Sign-in was cancelled', description), () => reject(new Error(description)));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const returnedState = url.searchParams.get('state') ?? '';
|
|
71
|
+
const code = url.searchParams.get('code') ?? '';
|
|
72
|
+
if (!code || !sameState(returnedState, state)) {
|
|
73
|
+
finish(400, DONE_PAGE('Sign-in could not be verified', 'Please run rivine login again.'), () => reject(new Error('The sign-in response did not match this terminal. Try again.')));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
finish(200, DONE_PAGE('You are signed in', 'You can close this tab and return to your terminal.'), () => resolve(code));
|
|
77
|
+
});
|
|
78
|
+
server.on('error', (err) => {
|
|
79
|
+
reject(err.code === 'EADDRINUSE'
|
|
80
|
+
? new Error(`Port ${port} is already in use — another rivine login may be running. ` +
|
|
81
|
+
`Close it and try again.`)
|
|
82
|
+
: err);
|
|
83
|
+
});
|
|
84
|
+
server.listen(port, '127.0.0.1');
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
server.close();
|
|
87
|
+
reject(new Error('Sign-in timed out. Run rivine login to try again.'));
|
|
88
|
+
}, timeoutMs);
|
|
89
|
+
timer.unref();
|
|
90
|
+
});
|
|
91
|
+
const bridge = new URL(config.authorize_bridge_url);
|
|
92
|
+
bridge.searchParams.set('challenge', challenge);
|
|
93
|
+
bridge.searchParams.set('state', state);
|
|
94
|
+
bridge.searchParams.set('port', String(port));
|
|
95
|
+
onUrl(bridge.toString());
|
|
96
|
+
openBrowser(bridge.toString());
|
|
97
|
+
const code = await received;
|
|
98
|
+
return exchangeCode(config, { code, verifier, redirectUri });
|
|
99
|
+
}
|
|
100
|
+
async function exchangeCode(config, args) {
|
|
101
|
+
const res = await fetch(`https://${config.auth0_domain}/oauth/token`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
104
|
+
body: new URLSearchParams({
|
|
105
|
+
grant_type: 'authorization_code',
|
|
106
|
+
client_id: config.auth0_client_id,
|
|
107
|
+
code: args.code,
|
|
108
|
+
code_verifier: args.verifier,
|
|
109
|
+
redirect_uri: args.redirectUri,
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
const detail = await res.text().catch(() => '');
|
|
114
|
+
throw new Error(`Could not complete sign-in (HTTP ${res.status}). ${detail.slice(0, 300)}`);
|
|
115
|
+
}
|
|
116
|
+
const body = (await res.json());
|
|
117
|
+
return {
|
|
118
|
+
accessToken: body.access_token,
|
|
119
|
+
refreshToken: body.refresh_token,
|
|
120
|
+
expiresAt: Date.now() + (body.expires_in ?? 3600) * 1000,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export async function refresh(config, refreshToken) {
|
|
124
|
+
const res = await fetch(`https://${config.auth0_domain}/oauth/token`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
127
|
+
body: new URLSearchParams({
|
|
128
|
+
grant_type: 'refresh_token',
|
|
129
|
+
client_id: config.auth0_client_id,
|
|
130
|
+
refresh_token: refreshToken,
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
throw new Error('Your session expired. Run rivine login to sign in again.');
|
|
135
|
+
}
|
|
136
|
+
const body = (await res.json());
|
|
137
|
+
return {
|
|
138
|
+
accessToken: body.access_token,
|
|
139
|
+
refreshToken: body.refresh_token ?? refreshToken,
|
|
140
|
+
expiresAt: Date.now() + (body.expires_in ?? 3600) * 1000,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Rivine Code CLI
|
|
3
|
+
* Licensed and owned by Beunec Technologies, Inc.
|
|
4
|
+
*
|
|
5
|
+
* https://aselius.beunec.com
|
|
6
|
+
*
|
|
7
|
+
* This software is proprietary. Use is governed by the licence agreement
|
|
8
|
+
* accompanying it; it is not open source and is not licensed for
|
|
9
|
+
* redistribution or derivative works.
|
|
10
|
+
*/
|
|
11
|
+
import { fetchCliConfig } from '../config.js';
|
|
12
|
+
import { clearSession, loadSession, saveSession, usingKeychain, } from './keychain.js';
|
|
13
|
+
import { loginWithPkce, refresh } from './pkce.js';
|
|
14
|
+
import { startDeviceFlow } from './device.js';
|
|
15
|
+
const REFRESH_SKEW_MS = 60_000;
|
|
16
|
+
let inFlight = null;
|
|
17
|
+
export class NotSignedIn extends Error {
|
|
18
|
+
constructor(message = 'You are not signed in. Run `rivine login`.') {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'NotSignedIn';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class SessionExpired extends NotSignedIn {
|
|
24
|
+
constructor() {
|
|
25
|
+
super('Your session expired and could not be renewed automatically. ' +
|
|
26
|
+
'Run `rivine login` to sign in again.');
|
|
27
|
+
this.name = 'SessionExpired';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function accessToken() {
|
|
31
|
+
const fromEnv = process.env.RIVINE_TOKEN?.trim();
|
|
32
|
+
if (fromEnv)
|
|
33
|
+
return fromEnv;
|
|
34
|
+
const stored = await loadSession();
|
|
35
|
+
if (!stored)
|
|
36
|
+
throw new NotSignedIn();
|
|
37
|
+
if (stored.expiresAt - REFRESH_SKEW_MS > Date.now()) {
|
|
38
|
+
return stored.accessToken;
|
|
39
|
+
}
|
|
40
|
+
if (!stored.refreshToken) {
|
|
41
|
+
await clearSession();
|
|
42
|
+
throw new SessionExpired();
|
|
43
|
+
}
|
|
44
|
+
if (!inFlight) {
|
|
45
|
+
inFlight = (async () => {
|
|
46
|
+
try {
|
|
47
|
+
const config = await fetchCliConfig();
|
|
48
|
+
const next = await refresh(config, stored.refreshToken);
|
|
49
|
+
await saveSession(next);
|
|
50
|
+
return next.accessToken;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
await clearSession();
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
inFlight = null;
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
}
|
|
61
|
+
return inFlight;
|
|
62
|
+
}
|
|
63
|
+
export async function isSignedIn() {
|
|
64
|
+
if (process.env.RIVINE_TOKEN?.trim())
|
|
65
|
+
return true;
|
|
66
|
+
return (await loadSession()) !== null;
|
|
67
|
+
}
|
|
68
|
+
export async function login(options = {}) {
|
|
69
|
+
const config = await fetchCliConfig();
|
|
70
|
+
let session;
|
|
71
|
+
if (options.device) {
|
|
72
|
+
const { start, poll } = await startDeviceFlow(config);
|
|
73
|
+
options.onDeviceCode?.(start.userCode, start.verificationUriComplete || start.verificationUri);
|
|
74
|
+
session = await poll();
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
session = await loginWithPkce(config, options.onUrl ?? (() => { }));
|
|
78
|
+
}
|
|
79
|
+
await saveSession(session);
|
|
80
|
+
if (!session.refreshToken) {
|
|
81
|
+
options.onNoRefreshToken?.(Math.max(1, Math.round((session.expiresAt - Date.now()) / 3_600_000)));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function logout() {
|
|
85
|
+
await clearSession();
|
|
86
|
+
}
|
|
87
|
+
export async function status() {
|
|
88
|
+
if (process.env.RIVINE_TOKEN?.trim()) {
|
|
89
|
+
return { signedIn: true, storage: 'environment' };
|
|
90
|
+
}
|
|
91
|
+
const stored = await loadSession();
|
|
92
|
+
const storage = (await usingKeychain()) ? 'keychain' : 'file';
|
|
93
|
+
return stored
|
|
94
|
+
? {
|
|
95
|
+
signedIn: true,
|
|
96
|
+
storage,
|
|
97
|
+
expiresAt: stored.expiresAt,
|
|
98
|
+
canRenew: Boolean(stored.refreshToken),
|
|
99
|
+
}
|
|
100
|
+
: { signedIn: false, storage };
|
|
101
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Rivine Code CLI
|
|
3
|
+
* Licensed and owned by Beunec Technologies, Inc.
|
|
4
|
+
*
|
|
5
|
+
* https://aselius.beunec.com
|
|
6
|
+
*
|
|
7
|
+
* This software is proprietary. Use is governed by the licence agreement
|
|
8
|
+
* accompanying it; it is not open source and is not licensed for
|
|
9
|
+
* redistribution or derivative works.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_API_URL = 'https://workspace.beunec.com';
|
|
12
|
+
const DEFAULT_SITE_URL = 'https://aselius.beunec.com';
|
|
13
|
+
function trimSlash(value) {
|
|
14
|
+
return value.trim().replace(/\/+$/, '');
|
|
15
|
+
}
|
|
16
|
+
export function apiUrl() {
|
|
17
|
+
return trimSlash(process.env.RIVINE_API_URL || DEFAULT_API_URL);
|
|
18
|
+
}
|
|
19
|
+
export function siteUrl() {
|
|
20
|
+
return trimSlash(process.env.RIVINE_SITE_URL || DEFAULT_SITE_URL);
|
|
21
|
+
}
|
|
22
|
+
let cached = null;
|
|
23
|
+
export async function fetchCliConfig() {
|
|
24
|
+
if (cached)
|
|
25
|
+
return cached;
|
|
26
|
+
const res = await fetch(`${apiUrl()}/api/cli/config`);
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const detail = await res.text().catch(() => '');
|
|
29
|
+
throw new Error(res.status === 503
|
|
30
|
+
? `Sign-in is not available right now: ${detail || 'the server is missing an Auth0 setting.'}`
|
|
31
|
+
: `Could not reach ${apiUrl()} (HTTP ${res.status}). Is RIVINE_API_URL correct?`);
|
|
32
|
+
}
|
|
33
|
+
cached = (await res.json());
|
|
34
|
+
return cached;
|
|
35
|
+
}
|
|
36
|
+
export const TARGET_ENGINE = 'rivine_code';
|
|
37
|
+
export const MEDIATOR_MODE = 'alpha';
|