lattris 0.1.4 → 0.2.1
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 +2 -2
- package/dist/build.json +1 -1
- package/dist/datasets/capture.js +118 -0
- package/dist/datasets/page.js +64 -0
- package/dist/datasets/pieces.js +78 -0
- package/dist/dispatch-form.js +202 -0
- package/dist/dispatch-words.js +133 -0
- package/dist/dispatch.js +67 -0
- package/dist/entry/session.js +219 -0
- package/dist/entry/set.js +130 -0
- package/dist/entry/setup-boot.js +151 -0
- package/dist/entry/setup.js +145 -0
- package/dist/entry/use.js +127 -0
- package/dist/help.js +65 -0
- package/dist/main.js +143 -6887
- package/dist/memory/access.js +88 -0
- package/dist/memory/capture-common.js +171 -0
- package/dist/memory/capture-help.js +195 -0
- package/dist/memory/capture-note.js +200 -0
- package/dist/memory/capture-session.js +168 -0
- package/dist/memory/delete.js +88 -0
- package/dist/memory/erase.js +67 -0
- package/dist/{extract-claude-jsonl.js → memory/extract-claude-jsonl.js} +5 -94
- package/dist/memory/get-help.js +72 -0
- package/dist/memory/get.js +246 -0
- package/dist/memory/history.js +134 -0
- package/dist/memory/recording-update.js +128 -0
- package/dist/memory/recording.js +258 -0
- package/dist/memory/resend.js +82 -0
- package/dist/memory/restore.js +73 -0
- package/dist/memory/save-session-chain.js +59 -0
- package/dist/memory/transcript.js +88 -0
- package/dist/memory/update-help.js +126 -0
- package/dist/memory/update.js +220 -0
- package/dist/memory/views.js +146 -0
- package/dist/nouns/consent.js +29 -0
- package/dist/nouns/contact.js +203 -0
- package/dist/nouns/datasets.js +109 -0
- package/dist/nouns/edges.js +106 -0
- package/dist/nouns/form.js +2 -0
- package/dist/nouns/index.js +48 -0
- package/dist/nouns/invite.js +98 -0
- package/dist/nouns/questions.js +116 -0
- package/dist/nouns/share.js +266 -0
- package/dist/nouns/words.js +81 -0
- package/dist/registry/datasets.js +15 -0
- package/dist/registry/entry.js +51 -0
- package/dist/registry/index.js +17 -0
- package/dist/registry/memory.js +116 -0
- package/dist/render/datasets.js +77 -0
- package/dist/render/door.js +39 -0
- package/dist/render/output.js +19 -0
- package/dist/render/prompt.js +4 -0
- package/dist/render/rows.js +82 -0
- package/dist/render/text.js +21 -0
- package/dist/request/access-token.js +67 -0
- package/dist/request/api-fetch.js +76 -0
- package/dist/request/callback.js +86 -0
- package/dist/request/door.js +74 -0
- package/dist/request/flags.js +38 -0
- package/dist/request/io.js +2 -0
- package/dist/request/paste-and-device.js +99 -0
- package/dist/request/stdin.js +22 -0
- package/dist/request/token.js +66 -0
- package/dist/request/upload.js +6 -0
- package/dist/request/version-check.js +79 -0
- package/dist/store/chains.js +25 -0
- package/dist/store/drafts.js +48 -0
- package/dist/store/environment.js +57 -0
- package/dist/store/exit.js +13 -0
- package/dist/store/io.js +2 -0
- package/dist/store/json-file.js +9 -0
- package/dist/store/session-home.js +189 -0
- package/dist/store/session-lock.js +169 -0
- package/dist/store/session.js +70 -0
- package/dist/store/write-identity.js +153 -0
- package/dist/version.js +16 -0
- package/package.json +3 -4
- package/dist/save-session-chain.js +0 -136
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
export const parsePastedAuth = (line, expectedState) => {
|
|
3
|
+
const params = new URLSearchParams(line.trim().replace(/^\?/, ''));
|
|
4
|
+
const code = params.get('code');
|
|
5
|
+
const state = params.get('state');
|
|
6
|
+
if (!code || !state)
|
|
7
|
+
return { kind: 'malformed' };
|
|
8
|
+
if (state !== expectedState)
|
|
9
|
+
return { kind: 'mismatch' };
|
|
10
|
+
return { kind: 'code', code };
|
|
11
|
+
};
|
|
12
|
+
export const waitForPastedCode = (expectedState, input, stderr) => {
|
|
13
|
+
const rl = createInterface({ input });
|
|
14
|
+
const code = new Promise((resolve) => {
|
|
15
|
+
rl.on('line', (line) => {
|
|
16
|
+
if (line.trim() === '')
|
|
17
|
+
return;
|
|
18
|
+
const verdict = parsePastedAuth(line, expectedState);
|
|
19
|
+
if (verdict.kind === 'code') {
|
|
20
|
+
rl.close();
|
|
21
|
+
resolve(verdict.code);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
stderr.write(verdict.kind === 'mismatch'
|
|
25
|
+
? 'That code is from a different sign-in attempt — copy it again from the current browser page.\n'
|
|
26
|
+
: 'That doesn’t look like the code — copy the whole box from the browser page.\n');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
code,
|
|
31
|
+
close: () => {
|
|
32
|
+
rl.close();
|
|
33
|
+
input.unref?.();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export const deviceAuthBody = (cfg) => ({
|
|
38
|
+
client_id: cfg.clientId,
|
|
39
|
+
scope: cfg.scopes,
|
|
40
|
+
audience: cfg.audience
|
|
41
|
+
});
|
|
42
|
+
export const requestDeviceAuth = async (network, cfg) => {
|
|
43
|
+
const res = await network(`${cfg.issuer}/oauth2/device/auth`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
46
|
+
body: new URLSearchParams(deviceAuthBody(cfg)).toString()
|
|
47
|
+
});
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const detail = await res.text().catch(() => '');
|
|
50
|
+
throw new Error(`device authorization endpoint ${res.status}: ${detail.slice(0, 300)}`);
|
|
51
|
+
}
|
|
52
|
+
return (await res.json());
|
|
53
|
+
};
|
|
54
|
+
const DEVICE_EXPIRED_MESSAGE = 'The sign-in code expired — run lattris login --device again.';
|
|
55
|
+
const deviceTokenAttempt = async (network, cfg, deviceCode) => {
|
|
56
|
+
const res = await network(`${cfg.issuer}/oauth2/token`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
59
|
+
body: new URLSearchParams({
|
|
60
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
61
|
+
device_code: deviceCode,
|
|
62
|
+
client_id: cfg.clientId
|
|
63
|
+
}).toString()
|
|
64
|
+
});
|
|
65
|
+
const body = (await res.json().catch(() => ({})));
|
|
66
|
+
if (res.ok)
|
|
67
|
+
return { kind: 'token', token: body };
|
|
68
|
+
switch (body.error) {
|
|
69
|
+
case 'authorization_pending':
|
|
70
|
+
return { kind: 'pending' };
|
|
71
|
+
case 'slow_down':
|
|
72
|
+
return { kind: 'slow_down' };
|
|
73
|
+
case 'expired_token':
|
|
74
|
+
return { kind: 'fail', message: DEVICE_EXPIRED_MESSAGE };
|
|
75
|
+
case 'access_denied':
|
|
76
|
+
return { kind: 'fail', message: 'Sign-in was denied.' };
|
|
77
|
+
default:
|
|
78
|
+
return {
|
|
79
|
+
kind: 'fail',
|
|
80
|
+
message: `token endpoint ${res.status}: ${String(body.error_description ?? body.error ?? '').slice(0, 200)}`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
export const pollDeviceToken = async (io, cfg, auth, sleepFn = (ms) => new Promise((r) => setTimeout(r, ms))) => {
|
|
85
|
+
let interval = (auth.interval ?? 5) * 1000;
|
|
86
|
+
const deadline = io.now() + auth.expires_in * 1000;
|
|
87
|
+
while (io.now() < deadline) {
|
|
88
|
+
const verdict = await deviceTokenAttempt(io.network, cfg, auth.device_code);
|
|
89
|
+
if (verdict.kind === 'token')
|
|
90
|
+
return verdict.token;
|
|
91
|
+
if (verdict.kind === 'fail')
|
|
92
|
+
throw new Error(verdict.message);
|
|
93
|
+
if (verdict.kind === 'slow_down')
|
|
94
|
+
interval += 5000;
|
|
95
|
+
await sleepFn(interval);
|
|
96
|
+
}
|
|
97
|
+
throw new Error(DEVICE_EXPIRED_MESSAGE);
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=paste-and-device.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
export const readAll = async (input) => {
|
|
3
|
+
const chunks = [];
|
|
4
|
+
for await (const chunk of input)
|
|
5
|
+
chunks.push(chunk);
|
|
6
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
7
|
+
return text.endsWith('\n') ? text.slice(0, -1) : text;
|
|
8
|
+
};
|
|
9
|
+
export const ask = (input, output, question) => new Promise((resolve) => {
|
|
10
|
+
const rl = createInterface({ input, output });
|
|
11
|
+
let answered = false;
|
|
12
|
+
rl.once('close', () => {
|
|
13
|
+
if (!answered)
|
|
14
|
+
resolve(null);
|
|
15
|
+
});
|
|
16
|
+
rl.question(question, (answer) => {
|
|
17
|
+
answered = true;
|
|
18
|
+
rl.close();
|
|
19
|
+
resolve(answer);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=stdin.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const emailFromIdToken = (idToken) => {
|
|
2
|
+
try {
|
|
3
|
+
const payload = JSON.parse(Buffer.from(idToken.split('.')[1] ?? '', 'base64url').toString());
|
|
4
|
+
return typeof payload.email === 'string' && payload.email !== '' ? payload.email : null;
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
export const loginIdentityGate = (homeDir, tokenEmail) => {
|
|
11
|
+
const dirName = homeDir.split('/').filter(Boolean).at(-1) ?? '';
|
|
12
|
+
const homeEmail = dirName.includes('@') ? dirName : null;
|
|
13
|
+
if (!homeEmail)
|
|
14
|
+
return { save: true };
|
|
15
|
+
if (!tokenEmail) {
|
|
16
|
+
return {
|
|
17
|
+
save: false,
|
|
18
|
+
message: `Refusing to save: this directory is keyed to ${homeEmail}, but the sign-in ` +
|
|
19
|
+
`returned no email evidence. Nothing was written — retry the login, or use a ` +
|
|
20
|
+
`non-email --as home.\n`
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (homeEmail.toLowerCase() !== tokenEmail.toLowerCase()) {
|
|
24
|
+
return {
|
|
25
|
+
save: false,
|
|
26
|
+
message: `Refusing to save: this directory belongs to ${homeEmail}, but you signed in as ` +
|
|
27
|
+
`${tokenEmail}. Nothing was written — log in again as ${homeEmail}, ` +
|
|
28
|
+
`or point --as at ${tokenEmail} instead (\`lattris login\`).\n`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { save: true };
|
|
32
|
+
};
|
|
33
|
+
export const toSession = (t, now) => ({
|
|
34
|
+
accessToken: t.access_token,
|
|
35
|
+
refreshToken: t.refresh_token,
|
|
36
|
+
tokenType: t.token_type || 'bearer',
|
|
37
|
+
expiresAt: Math.floor(now / 1000) + (t.expires_in ?? 0)
|
|
38
|
+
});
|
|
39
|
+
export const postToken = async (network, cfg, body, signal) => {
|
|
40
|
+
const res = await network(`${cfg.issuer}/oauth2/token`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
43
|
+
body: new URLSearchParams(body).toString(),
|
|
44
|
+
...(signal ? { signal } : {})
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const detail = await res.text().catch(() => '');
|
|
48
|
+
throw new Error(`token endpoint ${res.status}: ${detail.slice(0, 300)}`);
|
|
49
|
+
}
|
|
50
|
+
return (await res.json());
|
|
51
|
+
};
|
|
52
|
+
export const refreshBody = (cfg, refreshToken) => ({
|
|
53
|
+
grant_type: 'refresh_token',
|
|
54
|
+
refresh_token: refreshToken,
|
|
55
|
+
client_id: cfg.clientId,
|
|
56
|
+
scope: cfg.scopes
|
|
57
|
+
});
|
|
58
|
+
export const authCodeExchangeBody = (cfg, code, codeVerifier, redirectUri) => ({
|
|
59
|
+
grant_type: 'authorization_code',
|
|
60
|
+
code,
|
|
61
|
+
redirect_uri: redirectUri,
|
|
62
|
+
client_id: cfg.clientId,
|
|
63
|
+
code_verifier: codeVerifier
|
|
64
|
+
});
|
|
65
|
+
export const hostedRedirectUri = (cfg) => `${cfg.webUrl}/cli/auth`;
|
|
66
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readJsonObject } from "../store/json-file.js";
|
|
3
|
+
const VERSION_CHECK_URL = 'https://registry.npmjs.org/lattris/latest';
|
|
4
|
+
const VERSION_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
const VERSION_CHECK_TIMEOUT_MS = 1500;
|
|
6
|
+
const parseVersion = (version) => {
|
|
7
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
|
|
8
|
+
if (!match)
|
|
9
|
+
return undefined;
|
|
10
|
+
return {
|
|
11
|
+
core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])],
|
|
12
|
+
prerelease: match[4] === undefined ? null : match[4].split('.')
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
const compareVersions = (left, right) => {
|
|
16
|
+
const a = parseVersion(left);
|
|
17
|
+
const b = parseVersion(right);
|
|
18
|
+
if (!a || !b)
|
|
19
|
+
return undefined;
|
|
20
|
+
for (let i = 0; i < a.core.length; i++) {
|
|
21
|
+
if (a.core[i] < b.core[i])
|
|
22
|
+
return -1;
|
|
23
|
+
if (a.core[i] > b.core[i])
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
if (a.prerelease === null && b.prerelease === null)
|
|
27
|
+
return 0;
|
|
28
|
+
if (a.prerelease === null)
|
|
29
|
+
return 1;
|
|
30
|
+
if (b.prerelease === null)
|
|
31
|
+
return -1;
|
|
32
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
33
|
+
for (let i = 0; i < length; i++) {
|
|
34
|
+
const av = a.prerelease[i];
|
|
35
|
+
const bv = b.prerelease[i];
|
|
36
|
+
if (av === undefined)
|
|
37
|
+
return -1;
|
|
38
|
+
if (bv === undefined)
|
|
39
|
+
return 1;
|
|
40
|
+
if (av === bv)
|
|
41
|
+
continue;
|
|
42
|
+
const aNumber = /^\d+$/.test(av);
|
|
43
|
+
const bNumber = /^\d+$/.test(bv);
|
|
44
|
+
if (aNumber && bNumber)
|
|
45
|
+
return BigInt(av) < BigInt(bv) ? -1 : 1;
|
|
46
|
+
if (aNumber !== bNumber)
|
|
47
|
+
return aNumber ? -1 : 1;
|
|
48
|
+
return av < bv ? -1 : 1;
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
};
|
|
52
|
+
export const checkForNewVersion = async (io, { currentVersion, stateDir, now }) => {
|
|
53
|
+
try {
|
|
54
|
+
const cachePath = join(stateDir, 'version-check.json');
|
|
55
|
+
const cache = readJsonObject(io.files, cachePath);
|
|
56
|
+
if (typeof cache?.checkedAt === 'number' && now - cache.checkedAt <= VERSION_CHECK_TTL_MS) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const response = await io.network(VERSION_CHECK_URL, {
|
|
60
|
+
headers: { Accept: 'application/json' },
|
|
61
|
+
signal: AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS)
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok)
|
|
64
|
+
return;
|
|
65
|
+
const body = (await response.json());
|
|
66
|
+
if (typeof body.version !== 'string' || parseVersion(body.version) === undefined)
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
io.files.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
70
|
+
io.files.writeFileSync(cachePath, `${JSON.stringify({ checkedAt: now, latestVersion: body.version })}\n`, { mode: 0o600 });
|
|
71
|
+
}
|
|
72
|
+
catch { }
|
|
73
|
+
if (compareVersions(currentVersion, body.version) === -1) {
|
|
74
|
+
io.stderr.write(`lattris ${currentVersion} → ${body.version} available: npm update -g lattris\n`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch { }
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=version-check.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { safeFileName, sessionDir } from "./session-home.js";
|
|
3
|
+
const receiptsPath = (store, conversationId) => {
|
|
4
|
+
const name = conversationId === null ? undefined : safeFileName(conversationId);
|
|
5
|
+
return name === undefined ? undefined : join(sessionDir(store), 'chains', `${name}.json`);
|
|
6
|
+
};
|
|
7
|
+
export const loadReceipts = (store, conversationId) => {
|
|
8
|
+
const path = receiptsPath(store, conversationId);
|
|
9
|
+
if (path === undefined)
|
|
10
|
+
return { epochs: {} };
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(store.files.readFileSync(path, 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return { epochs: {} };
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
export const saveReceipts = (store, conversationId, receipts) => {
|
|
19
|
+
const path = receiptsPath(store, conversationId);
|
|
20
|
+
if (path === undefined)
|
|
21
|
+
return;
|
|
22
|
+
store.files.mkdirSync(join(sessionDir(store), 'chains'), { recursive: true, mode: 0o700 });
|
|
23
|
+
store.files.writeFileSync(path, JSON.stringify(receipts, null, 2), { mode: 0o600 });
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=chains.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { safeFileName, sessionDir } from "./session-home.js";
|
|
4
|
+
const draftsDir = (store) => join(sessionDir(store), 'drafts');
|
|
5
|
+
const draftStamp = (store) => new Date(store.now()).toISOString().replace(/[:.]/g, '-');
|
|
6
|
+
const saveDraft = (store, stem, extension, contents) => {
|
|
7
|
+
const dir = draftsDir(store);
|
|
8
|
+
const name = safeFileName(stem);
|
|
9
|
+
if (name === undefined)
|
|
10
|
+
throw new Error(`could not keep a draft under ${dir}`);
|
|
11
|
+
const { chmodSync, mkdirSync, writeFileSync } = store.files;
|
|
12
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
13
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
14
|
+
const file = join(dir, `${name}-${draftStamp(store)}-${randomBytes(4).toString('hex')}.${extension}`);
|
|
15
|
+
try {
|
|
16
|
+
writeFileSync(file, contents, { mode: 0o600, flag: 'wx' });
|
|
17
|
+
chmodSync(file, 0o600);
|
|
18
|
+
return file;
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
if (e.code !== 'EEXIST')
|
|
22
|
+
throw e;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`could not keep a draft under ${dir}`);
|
|
26
|
+
};
|
|
27
|
+
export const saveRequestDraft = (store, kind, draft) => saveDraft(store, kind, 'json', `${JSON.stringify(draft, null, 2)}\n`);
|
|
28
|
+
export const saveEditDraft = (store, blockRef, draft) => {
|
|
29
|
+
const wordsPath = draft.fields.content !== undefined
|
|
30
|
+
? saveDraft(store, blockRef, 'md', `${draft.fields.content}\n`)
|
|
31
|
+
: undefined;
|
|
32
|
+
const draftPath = saveDraft(store, blockRef, 'json', `${JSON.stringify({ ...draft, wordsFile: wordsPath }, null, 2)}\n`);
|
|
33
|
+
return { draftPath, wordsPath };
|
|
34
|
+
};
|
|
35
|
+
export const alreadySaved = (sent, current) => {
|
|
36
|
+
if (sent.content !== undefined && sent.content !== current.content)
|
|
37
|
+
return false;
|
|
38
|
+
if (sent.title !== undefined && sent.title !== (current.title ?? undefined))
|
|
39
|
+
return false;
|
|
40
|
+
if (sent.aiSummary !== undefined && sent.aiSummary !== (current.aiSummary ?? undefined))
|
|
41
|
+
return false;
|
|
42
|
+
if (sent.intent !== undefined && (sent.intent ?? null) !== (current.intent ?? null))
|
|
43
|
+
return false;
|
|
44
|
+
if (sent.contributor !== undefined && sent.contributor !== (current.contributor?.id ?? null))
|
|
45
|
+
return false;
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=drafts.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { basename, dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { EXIT, refusal } from "./exit.js";
|
|
4
|
+
export const stripTrailingSlash = (s) => s.replace(/\/+$/, '');
|
|
5
|
+
export const defaultEnvironmentForEntry = (files, entryUrl) => {
|
|
6
|
+
const entryPath = fileURLToPath(entryUrl);
|
|
7
|
+
const entryDir = dirname(entryPath);
|
|
8
|
+
const isBuiltMain = basename(entryPath) === 'main.js' && basename(entryDir) === 'dist';
|
|
9
|
+
return isBuiltMain && !files.existsSync(join(entryDir, 'installed-check.js')) ? 'prod' : 'local';
|
|
10
|
+
};
|
|
11
|
+
const environmentsPath = (machine) => join(machine.env.XDG_CONFIG_HOME || join(machine.home, '.config'), 'lattris', 'environments.json');
|
|
12
|
+
const BUILTIN_LOCAL = {
|
|
13
|
+
issuer: 'http://localhost:4444',
|
|
14
|
+
api: 'http://localhost:54301',
|
|
15
|
+
web: 'http://localhost:4500',
|
|
16
|
+
clientId: 'lattris-cli'
|
|
17
|
+
};
|
|
18
|
+
export const BUILTIN_PROD = {
|
|
19
|
+
issuer: 'https://api.felix-synthetix.com',
|
|
20
|
+
api: 'https://lattris.com/api/lattris',
|
|
21
|
+
web: 'https://lattris.com',
|
|
22
|
+
clientId: 'lattris-cli'
|
|
23
|
+
};
|
|
24
|
+
const BUILTINS = { local: BUILTIN_LOCAL, prod: BUILTIN_PROD };
|
|
25
|
+
const environmentEntry = (machine, envName) => {
|
|
26
|
+
try {
|
|
27
|
+
const registry = JSON.parse(machine.files.readFileSync(environmentsPath(machine), 'utf8'));
|
|
28
|
+
if (typeof registry === 'object' && registry !== null && Object.hasOwn(registry, envName)) {
|
|
29
|
+
const entry = registry[envName];
|
|
30
|
+
if (entry)
|
|
31
|
+
return entry;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
return Object.hasOwn(BUILTINS, envName) ? BUILTINS[envName] : undefined;
|
|
36
|
+
};
|
|
37
|
+
export const knownEnvironment = (machine, envName) => environmentEntry(machine, envName) !== undefined;
|
|
38
|
+
const loadEnvironmentEntry = (machine, envName) => environmentEntry(machine, envName) ??
|
|
39
|
+
refusal(`Unknown environment: ${envName}. Known: local, prod (or add it to ${environmentsPath(machine)}).\n`, EXIT.USAGE);
|
|
40
|
+
export const loadConfig = (store) => {
|
|
41
|
+
const entry = loadEnvironmentEntry(store, store.envName);
|
|
42
|
+
if ('exit' in entry)
|
|
43
|
+
return entry;
|
|
44
|
+
const env = store.env;
|
|
45
|
+
return {
|
|
46
|
+
env: store.envName,
|
|
47
|
+
issuer: stripTrailingSlash(env.LATTRIS_ISSUER ?? entry.issuer),
|
|
48
|
+
apiUrl: stripTrailingSlash(env.LATTRIS_API ?? entry.api),
|
|
49
|
+
clientId: env.LATTRIS_CLIENT_ID ?? entry.clientId,
|
|
50
|
+
redirectPort: Number(env.LATTRIS_REDIRECT_PORT ?? '4577'),
|
|
51
|
+
audience: env.LATTRIS_AUDIENCE ?? 'foundations lattris',
|
|
52
|
+
scopes: env.LATTRIS_SCOPES ?? 'openid offline_access lattris:all:all',
|
|
53
|
+
webUrl: stripTrailingSlash(env.LATTRIS_WEB_URL ?? entry.web),
|
|
54
|
+
...(entry.remember_for !== undefined ? { rememberFor: entry.remember_for } : {})
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=environment.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const EXIT = {
|
|
2
|
+
OK: 0,
|
|
3
|
+
GENERIC: 1,
|
|
4
|
+
USAGE: 2,
|
|
5
|
+
NOAUTH: 3,
|
|
6
|
+
DOOR: 4
|
|
7
|
+
};
|
|
8
|
+
export const refusal = (message, exit = EXIT.USAGE) => ({
|
|
9
|
+
message,
|
|
10
|
+
exit
|
|
11
|
+
});
|
|
12
|
+
export const isRefusal = (value) => typeof value === 'object' && value !== null && 'exit' in value && 'message' in value;
|
|
13
|
+
//# sourceMappingURL=exit.js.map
|
package/dist/store/io.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
3
|
+
import { knownEnvironment } from "./environment.js";
|
|
4
|
+
export const stateHome = (machine) => machine.env.XDG_STATE_HOME || join(machine.home, '.local', 'state');
|
|
5
|
+
const stateDir = (machine) => join(stateHome(machine), 'lattris');
|
|
6
|
+
export const machinesRoot = (machine) => {
|
|
7
|
+
const named = machine.env.LATTRIS_MACHINES_ROOT;
|
|
8
|
+
return named
|
|
9
|
+
? resolve(machine.home, named.replace(/^~(?=\/|$)/, machine.home))
|
|
10
|
+
: join(machine.home, 'lattris-machines');
|
|
11
|
+
};
|
|
12
|
+
export const BINDING_FILE = '.lattris';
|
|
13
|
+
const madeHerePath = (machine) => join(stateDir(machine), 'bindings.json');
|
|
14
|
+
const present = (files, path) => {
|
|
15
|
+
try {
|
|
16
|
+
files.lstatSync(path);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const realPathOf = (files, path) => {
|
|
24
|
+
try {
|
|
25
|
+
return files.realpathSync(path);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
const parent = dirname(path);
|
|
29
|
+
return parent === path ? path : join(realPathOf(files, parent), basename(path));
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const bindingKey = (files, bindingFile) => join(realPathOf(files, dirname(bindingFile)), basename(bindingFile));
|
|
33
|
+
const underMachinesRoot = (machine, bindingFile, dir) => {
|
|
34
|
+
const root = realPathOf(machine.files, machinesRoot(machine));
|
|
35
|
+
const under = (path) => path.startsWith(root + sep);
|
|
36
|
+
const from = realPathOf(machine.files, dirname(bindingFile));
|
|
37
|
+
return under(realPathOf(machine.files, dir)) && from !== root && !under(from);
|
|
38
|
+
};
|
|
39
|
+
const digest = (content) => createHash('sha256').update(content).digest('hex');
|
|
40
|
+
const readMadeHere = (machine) => {
|
|
41
|
+
try {
|
|
42
|
+
const made = JSON.parse(machine.files.readFileSync(madeHerePath(machine), 'utf8'));
|
|
43
|
+
return typeof made === 'object' && made !== null ? made : {};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const wasMadeHere = (machine, bindingFile, content) => {
|
|
50
|
+
const made = readMadeHere(machine);
|
|
51
|
+
const key = bindingKey(machine.files, bindingFile);
|
|
52
|
+
return Object.hasOwn(made, key) && made[key] === digest(content);
|
|
53
|
+
};
|
|
54
|
+
const writeWhole = (machine, file, content, mode) => {
|
|
55
|
+
const draft = `${file}.${randomUUID()}.new`;
|
|
56
|
+
try {
|
|
57
|
+
machine.files.writeFileSync(draft, content, { mode, flag: 'wx' });
|
|
58
|
+
machine.files.renameSync(draft, file);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
if (e.code !== 'EEXIST') {
|
|
62
|
+
try {
|
|
63
|
+
machine.files.unlinkSync(draft);
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
}
|
|
67
|
+
throw e;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const saveMadeHere = (machine, made) => {
|
|
71
|
+
machine.files.mkdirSync(stateDir(machine), { recursive: true, mode: 0o700 });
|
|
72
|
+
writeWhole(machine, madeHerePath(machine), `${JSON.stringify(made, null, 2)}\n`, 0o600);
|
|
73
|
+
};
|
|
74
|
+
const readBinding = (machine, bindingFile) => {
|
|
75
|
+
const refused = { trusted: false, bindingFile };
|
|
76
|
+
let content;
|
|
77
|
+
try {
|
|
78
|
+
if (!machine.files.lstatSync(bindingFile).isFile())
|
|
79
|
+
return refused;
|
|
80
|
+
content = machine.files.readFileSync(bindingFile, 'utf8');
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return refused;
|
|
84
|
+
}
|
|
85
|
+
const lines = content
|
|
86
|
+
.trim()
|
|
87
|
+
.split('\n')
|
|
88
|
+
.map((l) => l.trim());
|
|
89
|
+
const pathLine = lines[0];
|
|
90
|
+
const env = lines
|
|
91
|
+
.slice(1)
|
|
92
|
+
.find((l) => l.startsWith('env='))
|
|
93
|
+
?.slice(4)
|
|
94
|
+
.trim();
|
|
95
|
+
if (!pathLine)
|
|
96
|
+
return refused;
|
|
97
|
+
if (env && !knownEnvironment(machine, env))
|
|
98
|
+
return refused;
|
|
99
|
+
const machineDir = resolve(dirname(bindingFile), pathLine);
|
|
100
|
+
if (!underMachinesRoot(machine, bindingFile, machineDir) &&
|
|
101
|
+
!wasMadeHere(machine, bindingFile, content))
|
|
102
|
+
return refused;
|
|
103
|
+
return { trusted: true, bindingFile, machineDir, ...(env ? { env } : {}) };
|
|
104
|
+
};
|
|
105
|
+
export const findBinding = (machine, from = machine.cwd) => {
|
|
106
|
+
let dir = from;
|
|
107
|
+
while (dir.startsWith(machine.home + sep)) {
|
|
108
|
+
const bindingFile = join(dir, BINDING_FILE);
|
|
109
|
+
if (present(machine.files, bindingFile))
|
|
110
|
+
return readBinding(machine, bindingFile);
|
|
111
|
+
const parent = dirname(dir);
|
|
112
|
+
if (parent === dir)
|
|
113
|
+
break;
|
|
114
|
+
dir = parent;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
};
|
|
118
|
+
export const writeBinding = (machine, dir, target, env) => {
|
|
119
|
+
const bindingFile = join(dir, BINDING_FILE);
|
|
120
|
+
const content = env ? `${target}\nenv=${env}\n` : `${target}\n`;
|
|
121
|
+
writeWhole(machine, bindingFile, content, 0o644);
|
|
122
|
+
if (underMachinesRoot(machine, bindingFile, target))
|
|
123
|
+
return;
|
|
124
|
+
saveMadeHere(machine, {
|
|
125
|
+
...readMadeHere(machine),
|
|
126
|
+
[bindingKey(machine.files, bindingFile)]: digest(content)
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
export const clearBinding = (machine, dir) => {
|
|
130
|
+
const bindingFile = join(dir, BINDING_FILE);
|
|
131
|
+
if (!present(machine.files, bindingFile))
|
|
132
|
+
return false;
|
|
133
|
+
const key = bindingKey(machine.files, bindingFile);
|
|
134
|
+
machine.files.rmSync(bindingFile);
|
|
135
|
+
const made = readMadeHere(machine);
|
|
136
|
+
if (Object.hasOwn(made, key)) {
|
|
137
|
+
delete made[key];
|
|
138
|
+
saveMadeHere(machine, made);
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
};
|
|
142
|
+
export const resolveActorDir = (files, actor, root) => {
|
|
143
|
+
const exact = join(root, actor);
|
|
144
|
+
if (actor.includes('@') || files.existsSync(exact))
|
|
145
|
+
return { dir: exact };
|
|
146
|
+
let entries = [];
|
|
147
|
+
try {
|
|
148
|
+
entries = files.readdirSync(root);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return { dir: exact };
|
|
152
|
+
}
|
|
153
|
+
const matches = entries.filter((e) => e.startsWith(`${actor}@`)).sort();
|
|
154
|
+
if (matches.length === 1)
|
|
155
|
+
return { dir: join(root, matches[0]) };
|
|
156
|
+
if (matches.length > 1)
|
|
157
|
+
return { ambiguous: matches };
|
|
158
|
+
return { dir: exact };
|
|
159
|
+
};
|
|
160
|
+
export const resolveSessionHome = (machine) => {
|
|
161
|
+
if (machine.env.LATTRIS_HOME)
|
|
162
|
+
return { dir: machine.env.LATTRIS_HOME };
|
|
163
|
+
const bound = findBinding(machine);
|
|
164
|
+
if (bound?.trusted)
|
|
165
|
+
return { dir: bound.machineDir, bindingFile: bound.bindingFile, bindingEnv: bound.env };
|
|
166
|
+
return { dir: stateDir(machine), ...(bound ? { untrusted: bound.bindingFile } : {}) };
|
|
167
|
+
};
|
|
168
|
+
export const sessionDir = (store) => {
|
|
169
|
+
const home = resolveSessionHome(store);
|
|
170
|
+
const isDefault = !home.bindingFile && !store.env.LATTRIS_HOME;
|
|
171
|
+
if (isDefault && !store.envExplicit)
|
|
172
|
+
return home.dir;
|
|
173
|
+
const envDir = join(home.dir, store.envName);
|
|
174
|
+
const files = store.files;
|
|
175
|
+
if (store.envName === 'local' &&
|
|
176
|
+
!files.existsSync(join(envDir, 'session.json')) &&
|
|
177
|
+
files.existsSync(join(home.dir, 'session.json'))) {
|
|
178
|
+
files.mkdirSync(envDir, { recursive: true, mode: 0o700 });
|
|
179
|
+
files.renameSync(join(home.dir, 'session.json'), join(envDir, 'session.json'));
|
|
180
|
+
files.chmodSync(join(envDir, 'session.json'), 0o600);
|
|
181
|
+
}
|
|
182
|
+
return envDir;
|
|
183
|
+
};
|
|
184
|
+
export const sessionPath = (store) => join(sessionDir(store), 'session.json');
|
|
185
|
+
export const safeFileName = (name) => {
|
|
186
|
+
const safe = name.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
187
|
+
return safe === '' || safe === '.' || safe === '..' ? undefined : safe;
|
|
188
|
+
};
|
|
189
|
+
//# sourceMappingURL=session-home.js.map
|