@hermit-org/cli 0.0.1-alpha.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 +73 -0
- package/README.zh-CN.md +72 -0
- package/package.json +22 -0
- package/src/commands/index.ts +54 -0
- package/src/commands/pair.ts +95 -0
- package/src/commands/start/index.ts +247 -0
- package/src/index.ts +17 -0
- package/src/lib/config.ts +142 -0
- package/src/lib/gateway.test.ts +143 -0
- package/src/lib/gateway.ts +404 -0
- package/src/lib/pairing.ts +106 -0
- package/src/lib/qr.ts +44 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
readHermitJson,
|
|
4
|
+
writeHermitJson,
|
|
5
|
+
loadConfig,
|
|
6
|
+
saveConfig,
|
|
7
|
+
} from "./config";
|
|
8
|
+
|
|
9
|
+
export interface PendingPair {
|
|
10
|
+
code: string;
|
|
11
|
+
token: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AuthorizedTokensFile {
|
|
16
|
+
tokens: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PENDING_PAIRS_FILE = "pending-pairs.json";
|
|
20
|
+
const AUTHORIZED_TOKENS_FILE = "authorized-tokens.json";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generate a random 6-digit pairing code.
|
|
24
|
+
*/
|
|
25
|
+
export function generatePairingCode(): string {
|
|
26
|
+
return String(Math.floor(100000 + Math.random() * 900000));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generate a secure bearer token.
|
|
31
|
+
*/
|
|
32
|
+
export function generateToken(): string {
|
|
33
|
+
return `tok_${randomBytes(24).toString("hex")}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a pending pairing request and return the human-readable code.
|
|
38
|
+
*/
|
|
39
|
+
export async function createPendingPair(): Promise<{
|
|
40
|
+
code: string;
|
|
41
|
+
token: string;
|
|
42
|
+
}> {
|
|
43
|
+
const code = generatePairingCode();
|
|
44
|
+
const token = generateToken();
|
|
45
|
+
const pending = (await readHermitJson<PendingPair[]>(PENDING_PAIRS_FILE)) ?? [];
|
|
46
|
+
|
|
47
|
+
// Remove expired entries older than 5 minutes.
|
|
48
|
+
const cutoff = Date.now() - 5 * 60 * 1000;
|
|
49
|
+
const fresh = pending.filter(
|
|
50
|
+
(p) => new Date(p.createdAt).getTime() > cutoff,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
fresh.push({ code, token, createdAt: new Date().toISOString() });
|
|
54
|
+
await writeHermitJson(PENDING_PAIRS_FILE, fresh);
|
|
55
|
+
|
|
56
|
+
return { code, token };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate a pairing code and return the corresponding bearer token.
|
|
61
|
+
*/
|
|
62
|
+
export async function validatePairingCode(
|
|
63
|
+
code: string,
|
|
64
|
+
): Promise<string | null> {
|
|
65
|
+
const pending = (await readHermitJson<PendingPair[]>(PENDING_PAIRS_FILE)) ?? [];
|
|
66
|
+
const match = pending.find((p) => p.code === code);
|
|
67
|
+
if (!match) return null;
|
|
68
|
+
|
|
69
|
+
// Authorize the token persistently.
|
|
70
|
+
await authorizeToken(match.token);
|
|
71
|
+
|
|
72
|
+
// Remove the used code.
|
|
73
|
+
const remaining = pending.filter((p) => p.code !== code);
|
|
74
|
+
await writeHermitJson(PENDING_PAIRS_FILE, remaining);
|
|
75
|
+
|
|
76
|
+
return match.token;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Add a token to the authorized list.
|
|
81
|
+
*/
|
|
82
|
+
export async function authorizeToken(token: string): Promise<void> {
|
|
83
|
+
const file =
|
|
84
|
+
(await readHermitJson<AuthorizedTokensFile>(AUTHORIZED_TOKENS_FILE)) ?? {
|
|
85
|
+
tokens: [],
|
|
86
|
+
};
|
|
87
|
+
if (!file.tokens.includes(token)) {
|
|
88
|
+
file.tokens.push(token);
|
|
89
|
+
await writeHermitJson(AUTHORIZED_TOKENS_FILE, file);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check whether a bearer token is authorized.
|
|
95
|
+
*/
|
|
96
|
+
export async function isTokenAuthorized(token: string): Promise<boolean> {
|
|
97
|
+
const file =
|
|
98
|
+
(await readHermitJson<AuthorizedTokensFile>(AUTHORIZED_TOKENS_FILE)) ?? {
|
|
99
|
+
tokens: [],
|
|
100
|
+
};
|
|
101
|
+
if (file.tokens.includes(token)) return true;
|
|
102
|
+
|
|
103
|
+
// Also allow tokens configured directly in hermit.config.json.
|
|
104
|
+
const config = await loadConfig();
|
|
105
|
+
return config.authorizedTokens?.includes(token) ?? false;
|
|
106
|
+
}
|
package/src/lib/qr.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import QRCode from "qrcode";
|
|
2
|
+
|
|
3
|
+
export interface ConnectionPayload {
|
|
4
|
+
url: string;
|
|
5
|
+
sendUrl: string;
|
|
6
|
+
token: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function encodeConnectionPayload(payload: ConnectionPayload): string {
|
|
10
|
+
return JSON.stringify(payload);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function decodeConnectionPayload(input: string): ConnectionPayload {
|
|
14
|
+
const trimmed = input.trim();
|
|
15
|
+
// Accept either raw JSON or a hermit://connect?payload=... deep link.
|
|
16
|
+
const deepLinkMatch = trimmed.match(/^hermit:\/\/connect\?payload=(.+)$/);
|
|
17
|
+
const json = deepLinkMatch ? decodeURIComponent(deepLinkMatch[1]) : trimmed;
|
|
18
|
+
return JSON.parse(json) as ConnectionPayload;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function generateQrTerminal(payload: ConnectionPayload): Promise<string> {
|
|
22
|
+
return QRCode.toString(encodeConnectionPayload(payload), {
|
|
23
|
+
type: "terminal",
|
|
24
|
+
errorCorrectionLevel: "M",
|
|
25
|
+
margin: 1,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function generateQrDataUrl(payload: ConnectionPayload): Promise<string> {
|
|
30
|
+
return QRCode.toDataURL(encodeConnectionPayload(payload), {
|
|
31
|
+
errorCorrectionLevel: "M",
|
|
32
|
+
margin: 2,
|
|
33
|
+
width: 400,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function generateQrBuffer(payload: ConnectionPayload): Promise<Buffer> {
|
|
38
|
+
return QRCode.toBuffer(encodeConnectionPayload(payload), {
|
|
39
|
+
errorCorrectionLevel: "M",
|
|
40
|
+
margin: 2,
|
|
41
|
+
width: 400,
|
|
42
|
+
type: "png",
|
|
43
|
+
});
|
|
44
|
+
}
|