@olares/lares-core 0.15.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.
Files changed (82) hide show
  1. package/README.md +23 -0
  2. package/brand/identity.d.ts +9 -0
  3. package/brand/identity.js +82 -0
  4. package/brand/manifest.js +15 -0
  5. package/drive/download.js +40 -0
  6. package/drive/execute.js +143 -0
  7. package/drive/files-path.js +55 -0
  8. package/drive/ls.js +126 -0
  9. package/drive/paths.js +320 -0
  10. package/drive/tools.js +206 -0
  11. package/drive/url-download.js +314 -0
  12. package/files/deliverables.js +32 -0
  13. package/files/disposition.js +20 -0
  14. package/files/filename.js +22 -0
  15. package/files/intake.js +217 -0
  16. package/files/limits.js +1 -0
  17. package/files/markdown.js +88 -0
  18. package/files/mention.js +64 -0
  19. package/files/preview-cache.js +100 -0
  20. package/files/preview-groups.js +21 -0
  21. package/files/preview-workspace.js +342 -0
  22. package/files/preview.js +305 -0
  23. package/files/upload-client.js +40 -0
  24. package/files/upload-http.js +22 -0
  25. package/files/upload.js +104 -0
  26. package/i18n/chat-model-switch.js +41 -0
  27. package/i18n/chat-model.js +40 -0
  28. package/i18n/files.js +39 -0
  29. package/i18n/mobile.js +115 -0
  30. package/i18n/preview.js +57 -0
  31. package/i18n/search.js +33 -0
  32. package/i18n/t.js +22 -0
  33. package/i18n/voice.js +70 -0
  34. package/icons/mark.js +22 -0
  35. package/larepass/chat.js +134 -0
  36. package/larepass/context-provenance.js +65 -0
  37. package/larepass/host.js +80 -0
  38. package/larepass/mux.js +194 -0
  39. package/larepass/questions.js +16 -0
  40. package/larepass/rpc.js +60 -0
  41. package/larepass/runtime.js +395 -0
  42. package/larepass/session-cache.js +104 -0
  43. package/larepass/settings.js +100 -0
  44. package/larepass/stage-copy.js +16 -0
  45. package/larepass/tool-row.js +132 -0
  46. package/larepass/transcript.js +332 -0
  47. package/media/ffmpeg-run.js +117 -0
  48. package/media/router-images.js +63 -0
  49. package/olares/entrance.js +45 -0
  50. package/olares/identity.js +190 -0
  51. package/olares/session-identity.js +48 -0
  52. package/olares/trusted-host.js +37 -0
  53. package/package.json +111 -0
  54. package/router/catalog-sync.js +87 -0
  55. package/router/catalog.d.ts +15 -0
  56. package/router/catalog.js +73 -0
  57. package/router/gateway.d.ts +4 -0
  58. package/router/gateway.js +21 -0
  59. package/router/models-client.js +39 -0
  60. package/router/models.d.ts +16 -0
  61. package/router/models.js +133 -0
  62. package/router/search.js +142 -0
  63. package/router/session-model.js +143 -0
  64. package/router/shim.js +134 -0
  65. package/router/stt.js +200 -0
  66. package/search/client.js +38 -0
  67. package/search/config.js +98 -0
  68. package/search/menu.js +22 -0
  69. package/tools/async.js +34 -0
  70. package/tools/crypto-uuid.js +26 -0
  71. package/tools/http.js +123 -0
  72. package/tools/json-file.js +18 -0
  73. package/voice/client.js +90 -0
  74. package/voice/config.js +59 -0
  75. package/voice/languages.js +29 -0
  76. package/voice/recorder.js +43 -0
  77. package/voice/service.js +83 -0
  78. package/workspace/env.js +13 -0
  79. package/workspace/home.js +9 -0
  80. package/workspace/path.js +80 -0
  81. package/workspace/seed.js +13 -0
  82. package/workspace/session.js +42 -0
@@ -0,0 +1,117 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { extname } from "node:path";
4
+
5
+ const LOG_LIMIT = 32_000;
6
+ const STDERR_LIMIT = 2000;
7
+ const SUBTITLE_STYLE = "FontName=Noto Sans CJK SC,FontSize=20,Outline=2,Shadow=0,MarginV=28";
8
+ const EVEN_DIMENSIONS = "pad=ceil(iw/2)*2:ceil(ih/2)*2";
9
+ const LIBX264_ARGV = [
10
+ "-c:v", "libx264",
11
+ "-preset", "veryfast",
12
+ "-crf", "23",
13
+ "-pix_fmt", "yuv420p",
14
+ ];
15
+
16
+ export function parseFfmpegReport(log, fallbackEncoder = "libx264") {
17
+ const text = String(log ?? "");
18
+ const speedMatch = [...text.matchAll(/\bspeed=\s*([0-9.]+)x\b/g)].at(-1);
19
+ const encoderMatch = text.match(/\[(libx264)\s@/i)
20
+ || text.match(/\bencoder:\s*(libx264)\b/i);
21
+ return {
22
+ encoder: encoderMatch?.[1] || fallbackEncoder,
23
+ speed: speedMatch ? `${speedMatch[1]}x` : "",
24
+ };
25
+ }
26
+
27
+ function escapeFilterValue(value) {
28
+ return String(value).replace(/([\\':,\[\];=\s])/g, "\\$1");
29
+ }
30
+
31
+ export function subtitleFilter(absolutePath) {
32
+ return `subtitles=filename=${escapeFilterValue(absolutePath)}:force_style='${SUBTITLE_STYLE}'`;
33
+ }
34
+
35
+ export function ffmpegEncodeArgv(job, { overwrite = false } = {}) {
36
+ const generated = Boolean(job.lavfi);
37
+ const args = ["-hide_banner", overwrite ? "-y" : "-n"];
38
+ if (generated) {
39
+ args.push("-f", "lavfi", "-i", job.lavfi);
40
+ } else {
41
+ args.push("-i", job.inputAbsolute);
42
+ }
43
+ if (job.duration != null) args.push("-t", String(job.duration));
44
+ const filters = [];
45
+ if (!generated) filters.push(EVEN_DIMENSIONS);
46
+ if (job.subtitlesAbsolute) filters.push(subtitleFilter(job.subtitlesAbsolute));
47
+ if (filters.length > 0) args.push("-vf", filters.join(","));
48
+ args.push(...LIBX264_ARGV);
49
+ if (!generated) args.push("-map", "0:v:0", "-map", "0:a:0?", "-c:a", "aac", "-b:a", "128k");
50
+ const ext = extname(job.absolutePath).toLowerCase();
51
+ if (ext === ".mp4") args.push("-movflags", "+faststart");
52
+ args.push(job.absolutePath);
53
+ return args;
54
+ }
55
+
56
+ function collectLog(stream, limit) {
57
+ let log = "";
58
+ stream?.on("data", (chunk) => {
59
+ log = `${log}${chunk}`.slice(-limit);
60
+ });
61
+ return () => log;
62
+ }
63
+
64
+ function ffmpegLogFailed(log) {
65
+ return /File ['"].*['"] already exists/i.test(log)
66
+ || /Conversion failed!/i.test(log)
67
+ || /Error opening output file/i.test(log);
68
+ }
69
+
70
+ function runFfmpeg(args, {
71
+ ffmpegBin = "ffmpeg",
72
+ spawnFn = spawn,
73
+ signal,
74
+ } = {}) {
75
+ return new Promise((resolve, reject) => {
76
+ const child = spawnFn(ffmpegBin, args, {
77
+ env: process.env,
78
+ stdio: ["ignore", "pipe", "pipe"],
79
+ ...(signal ? { signal } : {}),
80
+ });
81
+ const stdout = collectLog(child.stdout, LOG_LIMIT);
82
+ const stderr = collectLog(child.stderr, LOG_LIMIT);
83
+ child.on("error", (error) => {
84
+ reject(new Error(`ffmpeg failed: ${error.message}`));
85
+ });
86
+ child.on("close", (code) => {
87
+ const log = `${stdout()}\n${stderr()}`;
88
+ if (code === 0 && !ffmpegLogFailed(log)) {
89
+ resolve(log);
90
+ return;
91
+ }
92
+ reject(new Error(`ffmpeg exited ${code}: ${log.trim().slice(-STDERR_LIMIT) || "no output"}`));
93
+ });
94
+ });
95
+ }
96
+
97
+ export async function encodeWorkspaceVideo(job, {
98
+ ffmpegBin = "ffmpeg",
99
+ spawnFn = spawn,
100
+ signal,
101
+ } = {}) {
102
+ if (!job.overwrite && existsSync(job.absolutePath)) {
103
+ throw new Error(`${job.absolutePath} already exists; pass overwrite or choose another destination`);
104
+ }
105
+ const args = ffmpegEncodeArgv(job, { overwrite: Boolean(job.overwrite) });
106
+ const log = await runFfmpeg(args, { ffmpegBin, spawnFn, signal });
107
+ const info = statSync(job.absolutePath);
108
+ if (!info.isFile() || info.size === 0) {
109
+ throw new Error("ffmpeg wrote an empty file");
110
+ }
111
+ const report = parseFfmpegReport(log);
112
+ return {
113
+ bytes: info.size,
114
+ encoder: report.encoder,
115
+ speed: report.speed || "",
116
+ };
117
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Router's local engines decode prompt images with llama.cpp's stb_image,
3
+ * which has no WebP decoder: a WebP part is dropped from the prompt without an
4
+ * error and the model answers as though no image arrived. dsh sends each image
5
+ * in the media type it was uploaded as, so this gateway — the one hop every
6
+ * image bound for Router takes, whatever composer or tool produced it — is
7
+ * where the encoding is brought back into what the engines read.
8
+ */
9
+ import sharp from "sharp";
10
+
11
+ const WEBP_DATA_URL = /^data:image\/webp;base64,([a-z0-9+/=\s]*)$/i;
12
+ /** Parsing a chat body is only worth it when one of these is actually in it. */
13
+ const WEBP_MARKER = "data:image/webp";
14
+
15
+ /** @param {Buffer} body */
16
+ export function carriesWebpImage(body) {
17
+ return body.includes(WEBP_MARKER);
18
+ }
19
+
20
+ /**
21
+ * PNG preserves an icon's transparency; a photograph re-encoded losslessly
22
+ * would multiply the request body, so one without alpha goes to JPEG.
23
+ */
24
+ async function transcode(dataUrl) {
25
+ const match = WEBP_DATA_URL.exec(dataUrl);
26
+ if (match === null) return dataUrl;
27
+ const image = sharp(Buffer.from(match[1], "base64"));
28
+ const { hasAlpha } = await image.metadata();
29
+ const mediaType = hasAlpha ? "image/png" : "image/jpeg";
30
+ const bytes = await (hasAlpha ? image.png() : image.jpeg({ quality: 90 })).toBuffer();
31
+ return `data:${mediaType};base64,${bytes.toString("base64")}`;
32
+ }
33
+
34
+ /**
35
+ * Re-encode every WebP data URL the payload carries, wherever it sits: the
36
+ * OpenAI wire shape puts one under `image_url.url`, but a nested tool result
37
+ * or a future part spells its own path to the same string.
38
+ */
39
+ async function rewrite(value) {
40
+ if (typeof value === "string") return transcode(value);
41
+ if (Array.isArray(value)) return Promise.all(value.map((item) => rewrite(item)));
42
+ if (value === null || typeof value !== "object") return value;
43
+ const entries = await Promise.all(
44
+ Object.entries(value).map(async ([key, nested]) => [key, await rewrite(nested)]),
45
+ );
46
+ return Object.fromEntries(entries);
47
+ }
48
+
49
+ /**
50
+ * @param {Buffer} body - the chat request exactly as dsh sent it.
51
+ * @returns {Promise<Buffer>} the same request with WebP images re-encoded as
52
+ * PNG, or the original bytes when it cannot be rewritten.
53
+ */
54
+ export async function transcodeWebpImages(body) {
55
+ try {
56
+ const rewritten = await rewrite(JSON.parse(body.toString("utf8")));
57
+ return Buffer.from(JSON.stringify(rewritten), "utf8");
58
+ } catch (err) {
59
+ const message = err instanceof Error ? err.message : String(err);
60
+ console.warn(`[lares] WebP prompt image forwarded unconverted: ${message}`);
61
+ return body;
62
+ }
63
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Olares 入口域推导(client 侧共享源码,由各插件 client 构建时内联)。
3
+ *
4
+ * Olares 给每个应用入口分配 `<前缀>.<用户>.<zone>`:Lares 的前缀随安装随机,
5
+ * Router chart 固定申领第三级域名 `router`,两者共用同一 zone
6
+ * (公网 olares.com 与局域网 olares.local 各一套)。因此把当前页面域名的
7
+ * 首段换成 `router` 即 Router 控制台。
8
+ *
9
+ * 端口转发 / IP 直连等非入口访问下无法推断,返回空串,调用方应隐藏入口。
10
+ */
11
+
12
+ export function loopbackWebUrl(port) {
13
+ if (port === undefined || port === null || port === "") {
14
+ throw new Error("webServer missing while resolving surface URL");
15
+ }
16
+ return `http://127.0.0.1:${String(port)}`;
17
+ }
18
+
19
+ const ROUTER_LABEL = "router";
20
+ const OLARES_ZONES = new Set(["olares.com", "olares.local"]);
21
+ const DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
22
+ // Router 控制台是 history 模式 SPA,入口即顶层路由路径。
23
+ const ROUTER_ROUTE = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*$/;
24
+
25
+ /**
26
+ * @param {string} [route] Router 控制台顶层路由,如 llm / audio / tools
27
+ * @param {string} [hostname] 当前页面 hostname(不含端口)
28
+ * @returns {string} Router 控制台 URL;无法推断时为空串
29
+ */
30
+ export function routerConsoleUrl(route = "", hostname = globalThis.location?.hostname ?? "") {
31
+ const labels = String(hostname).trim().toLowerCase().split(".");
32
+ if (labels.length !== 4 || labels.some((label) => !DNS_LABEL.test(label))) return "";
33
+ if (!OLARES_ZONES.has(labels.slice(-2).join("."))) return "";
34
+ const path = String(route).trim().toLowerCase();
35
+ if (path && !ROUTER_ROUTE.test(path)) return "";
36
+ const origin = `https://${[ROUTER_LABEL, ...labels.slice(1)].join(".")}`;
37
+ return path ? `${origin}/${path}` : origin;
38
+ }
39
+
40
+ /** 新标签页打开 Router 控制台;无入口时静默忽略。 */
41
+ export function openRouterConsole(route = "") {
42
+ const url = routerConsoleUrl(route);
43
+ if (!url) return;
44
+ globalThis.open?.(url, "_blank", "noopener,noreferrer");
45
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Olares edge identity → olares-cli profile (from test_lares).
3
+ * Access JWT only; no refresh. Commands work until the cookie expires.
4
+ */
5
+ import { createCipheriv, randomBytes } from "node:crypto";
6
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+
9
+ const MASTER_KEY_BYTES = 32;
10
+ const IV_BYTES = 12;
11
+ const SERVICE = "olares-cli";
12
+
13
+ /**
14
+ * @typedef {{ user: string, token: string, terminus: string }} OlaresIdentity
15
+ */
16
+
17
+ /** @param {OlaresIdentity} identity */
18
+ export function identityAvailable(identity) {
19
+ return Boolean(identity.user && identity.token && identity.terminus);
20
+ }
21
+
22
+ /** @param {string} cookieHeader */
23
+ function parseCookies(cookieHeader) {
24
+ /** @type {Record<string, string>} */
25
+ const out = {};
26
+ for (const part of cookieHeader.split(";")) {
27
+ if (!part.includes("=")) continue;
28
+ const [name, ...rest] = part.split("=");
29
+ out[name.trim()] = rest.join("=").trim();
30
+ }
31
+ return out;
32
+ }
33
+
34
+ /** @param {string} user */
35
+ function userLabel(user) {
36
+ return user.split("@", 1)[0]?.trim().toLowerCase() ?? "";
37
+ }
38
+
39
+ /**
40
+ * @param {string} hostHeader
41
+ * @param {string} user
42
+ */
43
+ function terminusFromHost(hostHeader, user) {
44
+ const host = hostHeader.trim().split(":")[0]?.toLowerCase() ?? "";
45
+ const parts = host.split(".");
46
+ if (parts.length < 3) return "";
47
+ const terminus = parts.slice(1).join(".");
48
+ const label = userLabel(user);
49
+ if (!label || terminus.split(".", 1)[0] !== label) return "";
50
+ return terminus;
51
+ }
52
+
53
+ /**
54
+ * @param {import('node:http').IncomingHttpHeaders | Headers | Record<string, string | string[] | undefined>} headers
55
+ * @param {string | null} [userDomain]
56
+ * @returns {OlaresIdentity}
57
+ */
58
+ export function identityFromHeaders(headers, userDomain) {
59
+ const get = (name) => {
60
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
61
+ return headers.get(name) ?? "";
62
+ }
63
+ const raw = /** @type {Record<string, string | string[] | undefined>} */ (headers)[name]
64
+ ?? /** @type {Record<string, string | string[] | undefined>} */ (headers)[name.toLowerCase()];
65
+ if (Array.isArray(raw)) return raw.join(",");
66
+ return raw ?? "";
67
+ };
68
+
69
+ const user = (
70
+ get("remote-user")
71
+ || get("authelia-remote-user")
72
+ || get("x-bfl-user")
73
+ || ""
74
+ ).trim();
75
+
76
+ const cookies = parseCookies(get("cookie"));
77
+ let token = (cookies.auth_token ?? "").trim();
78
+ if (!token) {
79
+ const authorization = get("authorization").trim();
80
+ if (authorization.toLowerCase().startsWith("bearer ")) {
81
+ token = authorization.slice(7).trim();
82
+ } else if (authorization) {
83
+ token = authorization;
84
+ }
85
+ }
86
+ if (!token) token = get("x-authorization").trim();
87
+
88
+ let terminus = (userDomain ?? "").trim();
89
+ if (!terminus) terminus = terminusFromHost(get("x-forwarded-host"), user);
90
+ if (!terminus && user) terminus = `${userLabel(user)}.olares.com`;
91
+
92
+ return { user, token, terminus };
93
+ }
94
+
95
+ /** @param {string} account */
96
+ function safeFileName(account) {
97
+ return `${account.replace(/[^a-zA-Z0-9._-]/g, "_")}.enc`;
98
+ }
99
+
100
+ /**
101
+ * @param {string} plaintext
102
+ * @param {Buffer} key
103
+ */
104
+ function encryptAesGcm(plaintext, key) {
105
+ const iv = randomBytes(IV_BYTES);
106
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
107
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
108
+ const tag = cipher.getAuthTag();
109
+ return Buffer.concat([iv, ciphertext, tag]);
110
+ }
111
+
112
+ /** @param {OlaresIdentity} identity */
113
+ function ownerHome(identity) {
114
+ const root = (
115
+ process.env.LARES_CLI_ROOT
116
+ ?? process.env.OLARES_CLI_ROOT
117
+ ?? "/data/cli"
118
+ ).replace(/\/+$/, "");
119
+ const label = userLabel(identity.user) || "anonymous";
120
+ return join(root, label);
121
+ }
122
+
123
+ /**
124
+ * Materialize per-user olares-cli profile + Linux file keychain from the edge
125
+ * access token so bash/`olares-cli` authenticates without interactive login.
126
+ *
127
+ * @param {OlaresIdentity} identity
128
+ */
129
+ export function ensureCliProfile(identity) {
130
+ if (!identityAvailable(identity)) {
131
+ throw new Error("Olares edge identity incomplete (need user + auth_token + terminus)");
132
+ }
133
+
134
+ const home = ownerHome(identity);
135
+ const configDir = join(home, ".olares-cli");
136
+ const dataDir = join(home, "keychain");
137
+ const serviceDir = join(dataDir, SERVICE);
138
+ mkdirSync(configDir, { recursive: true, mode: 0o700 });
139
+ mkdirSync(serviceDir, { recursive: true, mode: 0o700 });
140
+
141
+ const configPath = join(configDir, "config.json");
142
+ const config = {
143
+ currentProfile: identity.user,
144
+ profiles: [
145
+ {
146
+ olaresId: identity.user,
147
+ backendVersion: "1.12.7",
148
+ backendVersionRefreshedAt: Math.floor(Date.now() / 1000),
149
+ },
150
+ ],
151
+ };
152
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
153
+
154
+ const masterPath = join(serviceDir, "master.key");
155
+ let master;
156
+ if (existsSync(masterPath)) {
157
+ master = readFileSync(masterPath);
158
+ if (master.length !== MASTER_KEY_BYTES) {
159
+ master = randomBytes(MASTER_KEY_BYTES);
160
+ writeFileSync(masterPath, master, { mode: 0o600 });
161
+ }
162
+ } else {
163
+ master = randomBytes(MASTER_KEY_BYTES);
164
+ writeFileSync(masterPath, master, { mode: 0o600 });
165
+ }
166
+
167
+ const stored = {
168
+ olaresId: identity.user,
169
+ accessToken: identity.token,
170
+ refreshToken: "",
171
+ grantedAt: Date.now(),
172
+ invalidatedAt: 0,
173
+ };
174
+ const enc = encryptAesGcm(JSON.stringify(stored), master);
175
+ const tokenPath = join(serviceDir, safeFileName(identity.user));
176
+ const tmp = `${tokenPath}.${process.pid}.tmp`;
177
+ writeFileSync(tmp, enc, { mode: 0o600 });
178
+ renameSync(tmp, tokenPath);
179
+
180
+ return {
181
+ home,
182
+ dataDir,
183
+ env: {
184
+ HOME: home,
185
+ OLARES_CLI_HOME: configDir,
186
+ OLARES_CLI_DATA_DIR: dataDir,
187
+ OLARES_CLI_REMOTE_ONLY: "1",
188
+ },
189
+ };
190
+ }
@@ -0,0 +1,48 @@
1
+ import {
2
+ ensureCliProfile,
3
+ identityAvailable,
4
+ } from "./identity.js";
5
+
6
+ /** @type {Map<string, import('./identity.js').OlaresIdentity>} */
7
+ const bySession = new Map();
8
+
9
+ /** @type {import('./identity.js').OlaresIdentity | null} */
10
+ let latest = null;
11
+
12
+ /**
13
+ * Cache edge identity and materialize olares-cli env (HOME / OLARES_CLI_*) for bash.
14
+ * @param {string} sessionId
15
+ * @param {import('./identity.js').OlaresIdentity} identity
16
+ */
17
+ export function rememberSessionIdentity(sessionId, identity) {
18
+ if (sessionId) bySession.set(sessionId, identity);
19
+ latest = identity;
20
+ if (!identityAvailable(identity)) return;
21
+ const profile = ensureCliProfile(identity);
22
+ Object.assign(process.env, profile.env);
23
+ }
24
+
25
+ /**
26
+ * Apply identity from a browser request when no session id is known yet.
27
+ * @param {import('./identity.js').OlaresIdentity} identity
28
+ */
29
+ export function rememberRequestIdentity(identity) {
30
+ latest = identity;
31
+ if (!identityAvailable(identity)) return;
32
+ const profile = ensureCliProfile(identity);
33
+ Object.assign(process.env, profile.env);
34
+ }
35
+
36
+ /** @param {string} sessionId */
37
+ export function getSessionIdentity(sessionId) {
38
+ return bySession.get(sessionId) ?? null;
39
+ }
40
+
41
+ export function getLatestIdentity() {
42
+ return latest;
43
+ }
44
+
45
+ /** @param {string} sessionId */
46
+ export function forgetSessionIdentity(sessionId) {
47
+ bySession.delete(sessionId);
48
+ }
@@ -0,0 +1,37 @@
1
+ export function trustedEntranceHosts(env = process.env) {
2
+ return (env.DSH_TRUSTED_HOSTS ?? "")
3
+ .split(",")
4
+ .map((s) => s.trim().toLowerCase())
5
+ .filter(Boolean);
6
+ }
7
+
8
+ /** Hostname of a bare `host[:port]` authority, or null when unparsable. */
9
+ export function hostnameOf(authority) {
10
+ try {
11
+ return new URL(`http://${authority}`).hostname.toLowerCase();
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ export function viaOlaresEntrance(hostHeader, entranceHosts) {
18
+ if (!hostHeader) return false;
19
+ const hostname = hostnameOf(hostHeader);
20
+ if (!hostname) return false;
21
+ return entranceHosts.some((entry) => hostnameOf(entry) === hostname);
22
+ }
23
+
24
+ export function shouldRewriteApiLoopback(req, entranceHosts) {
25
+ const path = new URL(req.url ?? "/", "http://x").pathname;
26
+ if (!path.startsWith("/api")) return false;
27
+ return viaOlaresEntrance(req.headers.host, entranceHosts);
28
+ }
29
+
30
+ export function loopbackAuthority(port) {
31
+ return `127.0.0.1:${port}`;
32
+ }
33
+
34
+ export function applyLoopbackHeaders(headers, authority) {
35
+ headers.host = authority;
36
+ if (headers.origin !== undefined) headers.origin = `http://${authority}`;
37
+ }
package/package.json ADDED
@@ -0,0 +1,111 @@
1
+ {
2
+ "name": "@olares/lares-core",
3
+ "version": "0.15.0",
4
+ "description": "Lares 共享业务核心:Router、workspace、文件/媒体、Olares 身份(无 UI、无平台壳)",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/beclab/lares.git",
9
+ "directory": "packages/core"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public",
13
+ "registry": "https://registry.npmjs.org/"
14
+ },
15
+ "files": [
16
+ "brand",
17
+ "drive",
18
+ "files",
19
+ "i18n",
20
+ "icons",
21
+ "larepass",
22
+ "media",
23
+ "olares",
24
+ "router",
25
+ "search",
26
+ "tools",
27
+ "voice",
28
+ "workspace"
29
+ ],
30
+ "exports": {
31
+ "./tools/http": "./tools/http.js",
32
+ "./tools/json-file": "./tools/json-file.js",
33
+ "./tools/async": "./tools/async.js",
34
+ "./tools/crypto-uuid": "./tools/crypto-uuid.js",
35
+ "./brand/identity": "./brand/identity.js",
36
+ "./brand/manifest": "./brand/manifest.js",
37
+ "./icons/mark": "./icons/mark.js",
38
+ "./i18n/t": "./i18n/t.js",
39
+ "./i18n/chat-model": "./i18n/chat-model.js",
40
+ "./i18n/chat-model-switch": "./i18n/chat-model-switch.js",
41
+ "./i18n/files": "./i18n/files.js",
42
+ "./i18n/voice": "./i18n/voice.js",
43
+ "./i18n/search": "./i18n/search.js",
44
+ "./i18n/preview": "./i18n/preview.js",
45
+ "./i18n/mobile": "./i18n/mobile.js",
46
+ "./router/catalog": "./router/catalog.js",
47
+ "./router/gateway": "./router/gateway.js",
48
+ "./router/models": {
49
+ "types": "./router/models.d.ts",
50
+ "default": "./router/models.js"
51
+ },
52
+ "./router/search": "./router/search.js",
53
+ "./router/shim": "./router/shim.js",
54
+ "./router/stt": "./router/stt.js",
55
+ "./router/catalog-sync": "./router/catalog-sync.js",
56
+ "./router/session-model": "./router/session-model.js",
57
+ "./workspace/env": "./workspace/env.js",
58
+ "./workspace/home": "./workspace/home.js",
59
+ "./workspace/path": "./workspace/path.js",
60
+ "./workspace/seed": "./workspace/seed.js",
61
+ "./workspace/session": "./workspace/session.js",
62
+ "./files/filename": "./files/filename.js",
63
+ "./files/upload": "./files/upload.js",
64
+ "./files/upload-http": "./files/upload-http.js",
65
+ "./files/limits": "./files/limits.js",
66
+ "./files/upload-client": "./files/upload-client.js",
67
+ "./files/preview": "./files/preview.js",
68
+ "./files/preview-workspace": "./files/preview-workspace.js",
69
+ "./files/preview-groups": "./files/preview-groups.js",
70
+ "./files/preview-cache": "./files/preview-cache.js",
71
+ "./files/markdown": "./files/markdown.js",
72
+ "./files/deliverables": "./files/deliverables.js",
73
+ "./files/disposition": "./files/disposition.js",
74
+ "./files/intake": "./files/intake.js",
75
+ "./files/mention": "./files/mention.js",
76
+ "./drive/files-path": "./drive/files-path.js",
77
+ "./drive/paths": "./drive/paths.js",
78
+ "./drive/download": "./drive/download.js",
79
+ "./drive/ls": "./drive/ls.js",
80
+ "./drive/url-download": "./drive/url-download.js",
81
+ "./drive/execute": "./drive/execute.js",
82
+ "./drive/tools": "./drive/tools.js",
83
+ "./voice/config": "./voice/config.js",
84
+ "./voice/languages": "./voice/languages.js",
85
+ "./voice/service": "./voice/service.js",
86
+ "./voice/client": "./voice/client.js",
87
+ "./voice/recorder": "./voice/recorder.js",
88
+ "./search/config": "./search/config.js",
89
+ "./search/menu": "./search/menu.js",
90
+ "./search/client": "./search/client.js",
91
+ "./router/models-client": "./router/models-client.js",
92
+ "./media/ffmpeg-run": "./media/ffmpeg-run.js",
93
+ "./media/router-images": "./media/router-images.js",
94
+ "./olares/identity": "./olares/identity.js",
95
+ "./olares/session-identity": "./olares/session-identity.js",
96
+ "./olares/entrance": "./olares/entrance.js",
97
+ "./olares/trusted-host": "./olares/trusted-host.js",
98
+ "./larepass/host": "./larepass/host.js",
99
+ "./larepass/rpc": "./larepass/rpc.js",
100
+ "./larepass/context-provenance": "./larepass/context-provenance.js",
101
+ "./larepass/tool-row": "./larepass/tool-row.js",
102
+ "./larepass/stage-copy": "./larepass/stage-copy.js",
103
+ "./larepass/transcript": "./larepass/transcript.js",
104
+ "./larepass/chat": "./larepass/chat.js",
105
+ "./larepass/mux": "./larepass/mux.js",
106
+ "./larepass/session-cache": "./larepass/session-cache.js",
107
+ "./larepass/runtime": "./larepass/runtime.js",
108
+ "./larepass/questions": "./larepass/questions.js",
109
+ "./larepass/settings": "./larepass/settings.js"
110
+ }
111
+ }