orbitchat 3.0.0 → 3.1.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 +93 -90
- package/bin/orbitchat.js +517 -430
- package/dist/assets/{ChartRenderer-B1mYF_kk.js → ChartRenderer-CapBCpbV.js} +1 -1
- package/dist/assets/{MermaidRenderer-Dhffx5mc.js → MermaidRenderer-DQJRirnm.js} +5 -5
- package/dist/assets/{MusicRenderer-DbXJ8e4-.js → MusicRenderer-9vnA54gg.js} +2 -2
- package/dist/assets/{SVGRenderer-EhamRX_K.js → SVGRenderer-By7CKtqU.js} +1 -1
- package/dist/assets/{_basePickBy-C2AibBqo.js → _basePickBy-Dm-zx8im.js} +1 -1
- package/dist/assets/{_baseUniq-D52pUSvn.js → _baseUniq-DLPvoy-d.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-BLMvVsZu.js → architectureDiagram-VXUJARFQ-B1kMlTMW.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-CTz0dv5u.js → blockDiagram-VD42YOAC-BlglDb-U.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-BjbN-GGh.js → c4Diagram-YG6GDRKO-CaZm--Yy.js} +1 -1
- package/dist/assets/channel-BjUDQMqf.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-DEMWGIsU.js → chunk-4BX2VUAB-4Kq-2AyN.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-BYaauHLT.js → chunk-55IACEB6-FApueVFk.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-D3_GFcfd.js → chunk-B4BG7PRW-CHox7be9.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-Dy2EuzfA.js → chunk-DI55MBZ5-N5sePLFo.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C9XHmwza.js → chunk-FMBD7UC4-BSbBs0xy.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-BOCltIi9.js → chunk-QN33PNHL-ByurI9-W.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-CvA964eQ.js → chunk-QZHKN3VN-Yy8VVut9.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-B-3rZz90.js → chunk-TZMSLE5B-B5oe8pT4.js} +1 -1
- package/dist/assets/classDiagram-2ON5EDUG-BhHCvLfM.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-BhHCvLfM.js +1 -0
- package/dist/assets/clone-oQOn4Sap.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-CzPip3he.js → cose-bilkent-S5V4N54A-DS_-k6qR.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-CfCXYus3.js → dagre-6UL2VRFP-DgkwJjVY.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-DyI2eDqp.js → diagram-PSM6KHXK-C9su64f9.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-DuakIh40.js → diagram-QEK2KX5R-DY6-9Dbc.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-C3-4IORM.js → diagram-S2PKOQOG-_Wub-Eik.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-Dt-nZEaD.js → erDiagram-Q2GNP2WA-Gwx9nSAM.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-B-2wjyTQ.js → flowDiagram-NV44I4VS-DmrXTF_S.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-CHTvjPq0.js → ganttDiagram-JELNMOA3-DymVUgsd.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-d5GWmgY4.js → gitGraphDiagram-V2S2FVAM-D3VSQFT4.js} +1 -1
- package/dist/assets/{graph-COgkbiU8.js → graph-BIe-N9yO.js} +1 -1
- package/dist/assets/index-B-NnBj1Y.js +624 -0
- package/dist/assets/index-BA5Mw76S.css +1 -0
- package/dist/assets/{index-DN2bBcCs.js → index-BiM6VNSE.js} +1 -1
- package/dist/assets/{infoDiagram-HS3SLOUP-30Hn0iLj.js → infoDiagram-HS3SLOUP-D2DeUrgk.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-CMOYXQ79.js → journeyDiagram-XKPGCS4Q-GxFhv1XB.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-dt3376cq.js → kanban-definition-3W4ZIXB7-BhT-ga9I.js} +1 -1
- package/dist/assets/{layout-DH97sy5g.js → layout-38O87Ivt.js} +1 -1
- package/dist/assets/{mindmap-definition-VGOIOE7T-BcZHPAlA.js → mindmap-definition-VGOIOE7T-DfKjmcaV.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-DXjvgcgl.js → pieDiagram-ADFJNKIX-D7lDWRGN.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-CICk2FdS.js → quadrantDiagram-AYHSOK5B-CPtZ8LgW.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-BdtxpQRA.js → requirementDiagram-UZGBJVZJ-h-H7R8sR.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-BVNS9BQJ.js → sankeyDiagram-TZEHDZUN-C6cn5g4m.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-DF4fJTM7.js → sequenceDiagram-WL72ISMW-B6AwcQ4F.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-C8H8HujX.js → stateDiagram-FKZM4ZOC-B4ne7z3I.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-C_qp8U90.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-DUSG5Vfy.js → timeline-definition-IT6M3QCI-BmjJnJ-r.js} +1 -1
- package/dist/assets/treemap-GDKQZRPO-CBUbyJIX.js +160 -0
- package/dist/assets/{xychartDiagram-PRI3JC2R-CFPJj8WK.js → xychartDiagram-PRI3JC2R-C3lVgboM.js} +1 -1
- package/dist/index.html +2 -2
- package/orbitchat.sh +305 -0
- package/package.json +6 -2
- package/dist/assets/channel-ryTtfXZE.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG-VbShFIzz.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB-VbShFIzz.js +0 -1
- package/dist/assets/clone-BlhdKVDQ.js +0 -1
- package/dist/assets/index-DtztdW2a.js +0 -643
- package/dist/assets/index-lGqyWNWb.css +0 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-DwtU033b.js +0 -1
- package/dist/assets/treemap-GDKQZRPO-1PI2PY4S.js +0 -160
package/bin/orbitchat.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* ORBIT Chat CLI
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* Serves the chat-app as a standalone application with runtime configuration.
|
|
6
|
-
* Configuration
|
|
7
|
-
*
|
|
6
|
+
* Configuration is read from orbitchat.yaml (CWD by default, overridable via --config).
|
|
7
|
+
* Secrets (adapter API keys) come from VITE_ADAPTERS / ORBIT_ADAPTERS env var.
|
|
8
|
+
*
|
|
8
9
|
* The server acts as a proxy to hide API keys from the client by mapping
|
|
9
10
|
* adapter names to actual API keys.
|
|
10
11
|
*/
|
|
@@ -12,34 +13,101 @@
|
|
|
12
13
|
import express from 'express';
|
|
13
14
|
import fs from 'fs';
|
|
14
15
|
import path from 'path';
|
|
15
|
-
import { homedir } from 'os';
|
|
16
16
|
import { execSync } from 'child_process';
|
|
17
17
|
import { fileURLToPath } from 'url';
|
|
18
18
|
import { dirname } from 'path';
|
|
19
19
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
20
|
+
import yaml from 'js-yaml';
|
|
21
|
+
import rateLimit from 'express-rate-limit';
|
|
20
22
|
|
|
21
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
22
24
|
const __dirname = dirname(__filename);
|
|
23
25
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
// ---- Minimal .env loader (CLI mode) ----
|
|
27
|
+
|
|
28
|
+
function parseDotEnvValue(raw) {
|
|
29
|
+
const trimmed = raw.trim();
|
|
30
|
+
if (!trimmed) return '';
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
34
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
35
|
+
) {
|
|
36
|
+
return trimmed.slice(1, -1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return trimmed;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadDotEnvFromFile(filePath) {
|
|
43
|
+
if (!fs.existsSync(filePath)) return;
|
|
44
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
45
|
+
const lines = content.split(/\r?\n/);
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < lines.length; i++) {
|
|
48
|
+
const trimmed = lines[i].trim();
|
|
49
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
50
|
+
|
|
51
|
+
const equalsIndex = trimmed.indexOf('=');
|
|
52
|
+
if (equalsIndex <= 0) continue;
|
|
53
|
+
|
|
54
|
+
const key = trimmed.slice(0, equalsIndex).trim();
|
|
55
|
+
if (!key) continue;
|
|
56
|
+
|
|
57
|
+
if (process.env[key] !== undefined) continue;
|
|
58
|
+
|
|
59
|
+
let valueRaw = trimmed.slice(equalsIndex + 1);
|
|
60
|
+
const startsWithDouble = valueRaw.startsWith('"');
|
|
61
|
+
const startsWithSingle = valueRaw.startsWith("'");
|
|
62
|
+
|
|
63
|
+
// Support simple multiline quoted values, useful for formatted JSON values.
|
|
64
|
+
if (
|
|
65
|
+
(startsWithDouble && !valueRaw.endsWith('"')) ||
|
|
66
|
+
(startsWithSingle && !valueRaw.endsWith("'"))
|
|
67
|
+
) {
|
|
68
|
+
const quote = startsWithDouble ? '"' : "'";
|
|
69
|
+
while (i + 1 < lines.length) {
|
|
70
|
+
i += 1;
|
|
71
|
+
valueRaw += `\n${lines[i]}`;
|
|
72
|
+
if (lines[i].trim().endsWith(quote)) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const value = parseDotEnvValue(valueRaw);
|
|
79
|
+
process.env[key] = value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function loadDotEnv(baseDir) {
|
|
84
|
+
// Same precedence idea as Vite: .env then .env.local; do not override exported vars.
|
|
85
|
+
loadDotEnvFromFile(path.join(baseDir, '.env'));
|
|
86
|
+
loadDotEnvFromFile(path.join(baseDir, '.env.local'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- Defaults (must match DEFAULTS in src/utils/runtimeConfig.ts) ----
|
|
90
|
+
|
|
91
|
+
const DEFAULTS = {
|
|
27
92
|
apiUrl: 'http://localhost:3000',
|
|
28
93
|
defaultKey: 'default-key',
|
|
29
94
|
applicationName: 'ORBIT Chat',
|
|
30
95
|
applicationDescription: "Explore ideas with ORBIT's AI copilots, share context, and build together.",
|
|
31
96
|
defaultInputPlaceholder: 'Message ORBIT...',
|
|
32
|
-
useLocalApi: false,
|
|
33
|
-
localApiPath: undefined,
|
|
34
97
|
consoleDebug: false,
|
|
98
|
+
locale: 'en-US',
|
|
35
99
|
enableUploadButton: false,
|
|
36
100
|
enableAudioOutput: false,
|
|
37
101
|
enableAudioInput: false,
|
|
38
102
|
enableFeedbackButtons: false,
|
|
103
|
+
enableConversationThreads: true,
|
|
39
104
|
enableAutocomplete: false,
|
|
40
105
|
voiceSilenceTimeoutMs: 4000,
|
|
41
106
|
voiceRecognitionLanguage: '',
|
|
107
|
+
showGitHubStats: true,
|
|
42
108
|
outOfServiceMessage: null,
|
|
109
|
+
githubOwner: 'schmitech',
|
|
110
|
+
githubRepo: 'orbit',
|
|
43
111
|
maxFilesPerConversation: 5,
|
|
44
112
|
maxFileSizeMB: 50,
|
|
45
113
|
maxTotalFiles: 100,
|
|
@@ -48,61 +116,219 @@ const DEFAULT_CONFIG = {
|
|
|
48
116
|
maxMessagesPerThread: 1000,
|
|
49
117
|
maxTotalMessages: 10000,
|
|
50
118
|
maxMessageLength: 1000,
|
|
119
|
+
guestMaxConversations: 1,
|
|
120
|
+
guestMaxMessagesPerConversation: 10,
|
|
121
|
+
guestMaxTotalMessages: 10,
|
|
122
|
+
guestMaxMessagesPerThread: 10,
|
|
123
|
+
guestMaxFilesPerConversation: 1,
|
|
124
|
+
guestMaxTotalFiles: 2,
|
|
125
|
+
guestMaxMessageLength: 500,
|
|
126
|
+
guestMaxFileSizeMB: 10,
|
|
51
127
|
settingsAboutMsg: 'ORBIT Chat',
|
|
128
|
+
enableAuth: false,
|
|
129
|
+
authDomain: '',
|
|
130
|
+
authClientId: '',
|
|
131
|
+
authAudience: '',
|
|
132
|
+
enableHeader: false,
|
|
133
|
+
headerLogoUrl: '',
|
|
134
|
+
headerBrandName: '',
|
|
135
|
+
headerBgColor: '',
|
|
136
|
+
headerTextColor: '',
|
|
137
|
+
headerNavLinks: [],
|
|
138
|
+
enableFooter: false,
|
|
139
|
+
footerText: '',
|
|
140
|
+
footerBgColor: '',
|
|
141
|
+
footerTextColor: '',
|
|
142
|
+
footerNavLinks: [],
|
|
52
143
|
};
|
|
53
144
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
145
|
+
// ---- YAML config loading ----
|
|
146
|
+
|
|
147
|
+
function flattenYamlConfig(y) {
|
|
148
|
+
const f = {};
|
|
149
|
+
if (y.application) {
|
|
150
|
+
const a = y.application;
|
|
151
|
+
if (a.name !== undefined) f.applicationName = a.name;
|
|
152
|
+
if (a.description !== undefined) f.applicationDescription = a.description;
|
|
153
|
+
if (a.inputPlaceholder !== undefined) f.defaultInputPlaceholder = a.inputPlaceholder;
|
|
154
|
+
if (a.settingsAboutMsg !== undefined) f.settingsAboutMsg = a.settingsAboutMsg;
|
|
155
|
+
if (a.locale !== undefined) f.locale = a.locale;
|
|
156
|
+
}
|
|
157
|
+
if (y.api) {
|
|
158
|
+
if (y.api.url !== undefined) f.apiUrl = y.api.url;
|
|
159
|
+
if (y.api.defaultAdapter !== undefined) f.defaultKey = y.api.defaultAdapter;
|
|
160
|
+
}
|
|
161
|
+
if (y.debug) {
|
|
162
|
+
if (y.debug.consoleDebug !== undefined) f.consoleDebug = y.debug.consoleDebug;
|
|
163
|
+
}
|
|
164
|
+
if (y.features) {
|
|
165
|
+
const fe = y.features;
|
|
166
|
+
if (fe.enableUpload !== undefined) f.enableUploadButton = fe.enableUpload;
|
|
167
|
+
if (fe.enableAudioOutput !== undefined) f.enableAudioOutput = fe.enableAudioOutput;
|
|
168
|
+
if (fe.enableAudioInput !== undefined) f.enableAudioInput = fe.enableAudioInput;
|
|
169
|
+
if (fe.enableFeedbackButtons !== undefined) f.enableFeedbackButtons = fe.enableFeedbackButtons;
|
|
170
|
+
if (fe.enableConversationThreads !== undefined) f.enableConversationThreads = fe.enableConversationThreads;
|
|
171
|
+
if (fe.enableAutocomplete !== undefined) f.enableAutocomplete = fe.enableAutocomplete;
|
|
172
|
+
}
|
|
173
|
+
if (y.voice) {
|
|
174
|
+
if (y.voice.silenceTimeoutMs !== undefined) f.voiceSilenceTimeoutMs = y.voice.silenceTimeoutMs;
|
|
175
|
+
if (y.voice.recognitionLanguage !== undefined) f.voiceRecognitionLanguage = y.voice.recognitionLanguage;
|
|
176
|
+
}
|
|
177
|
+
if (y.github) {
|
|
178
|
+
if (y.github.showStats !== undefined) f.showGitHubStats = y.github.showStats;
|
|
179
|
+
if (y.github.owner !== undefined) f.githubOwner = y.github.owner;
|
|
180
|
+
if (y.github.repo !== undefined) f.githubRepo = y.github.repo;
|
|
181
|
+
}
|
|
182
|
+
if (y.outOfServiceMessage !== undefined) f.outOfServiceMessage = y.outOfServiceMessage;
|
|
183
|
+
if (y.limits) {
|
|
184
|
+
const l = y.limits;
|
|
185
|
+
if (l.files) {
|
|
186
|
+
if (l.files.perConversation !== undefined) f.maxFilesPerConversation = l.files.perConversation;
|
|
187
|
+
if (l.files.maxSizeMB !== undefined) f.maxFileSizeMB = l.files.maxSizeMB;
|
|
188
|
+
if (l.files.totalFiles !== undefined) f.maxTotalFiles = l.files.totalFiles;
|
|
189
|
+
}
|
|
190
|
+
if (l.conversations) {
|
|
191
|
+
if (l.conversations.maxConversations !== undefined) f.maxConversations = l.conversations.maxConversations;
|
|
192
|
+
if (l.conversations.messagesPerConversation !== undefined) f.maxMessagesPerConversation = l.conversations.messagesPerConversation;
|
|
193
|
+
if (l.conversations.messagesPerThread !== undefined) f.maxMessagesPerThread = l.conversations.messagesPerThread;
|
|
194
|
+
if (l.conversations.totalMessages !== undefined) f.maxTotalMessages = l.conversations.totalMessages;
|
|
195
|
+
}
|
|
196
|
+
if (l.messages) {
|
|
197
|
+
if (l.messages.maxLength !== undefined) f.maxMessageLength = l.messages.maxLength;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (y.guestLimits) {
|
|
201
|
+
const g = y.guestLimits;
|
|
202
|
+
if (g.files) {
|
|
203
|
+
if (g.files.perConversation !== undefined) f.guestMaxFilesPerConversation = g.files.perConversation;
|
|
204
|
+
if (g.files.maxSizeMB !== undefined) f.guestMaxFileSizeMB = g.files.maxSizeMB;
|
|
205
|
+
if (g.files.totalFiles !== undefined) f.guestMaxTotalFiles = g.files.totalFiles;
|
|
206
|
+
}
|
|
207
|
+
if (g.conversations) {
|
|
208
|
+
if (g.conversations.maxConversations !== undefined) f.guestMaxConversations = g.conversations.maxConversations;
|
|
209
|
+
if (g.conversations.messagesPerConversation !== undefined) f.guestMaxMessagesPerConversation = g.conversations.messagesPerConversation;
|
|
210
|
+
if (g.conversations.messagesPerThread !== undefined) f.guestMaxMessagesPerThread = g.conversations.messagesPerThread;
|
|
211
|
+
if (g.conversations.totalMessages !== undefined) f.guestMaxTotalMessages = g.conversations.totalMessages;
|
|
212
|
+
}
|
|
213
|
+
if (g.messages) {
|
|
214
|
+
if (g.messages.maxLength !== undefined) f.guestMaxMessageLength = g.messages.maxLength;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (y.auth) {
|
|
218
|
+
if (y.auth.enabled !== undefined) f.enableAuth = y.auth.enabled;
|
|
219
|
+
}
|
|
220
|
+
if (y.header) {
|
|
221
|
+
const h = y.header;
|
|
222
|
+
if (h.enabled !== undefined) f.enableHeader = h.enabled;
|
|
223
|
+
if (h.logoUrl !== undefined) f.headerLogoUrl = h.logoUrl;
|
|
224
|
+
if (h.brandName !== undefined) f.headerBrandName = h.brandName;
|
|
225
|
+
if (h.bgColor !== undefined) f.headerBgColor = h.bgColor;
|
|
226
|
+
if (h.textColor !== undefined) f.headerTextColor = h.textColor;
|
|
227
|
+
if (h.navLinks !== undefined) f.headerNavLinks = h.navLinks;
|
|
58
228
|
}
|
|
229
|
+
if (y.footer) {
|
|
230
|
+
const ft = y.footer;
|
|
231
|
+
if (ft.enabled !== undefined) f.enableFooter = ft.enabled;
|
|
232
|
+
if (ft.text !== undefined) f.footerText = ft.text;
|
|
233
|
+
if (ft.bgColor !== undefined) f.footerBgColor = ft.bgColor;
|
|
234
|
+
if (ft.textColor !== undefined) f.footerTextColor = ft.textColor;
|
|
235
|
+
if (ft.navLinks !== undefined) f.footerNavLinks = ft.navLinks;
|
|
236
|
+
}
|
|
237
|
+
// Adapters from YAML: include metadata (name, description, notes, apiUrl) but NOT apiKey
|
|
238
|
+
if (y.adapters !== undefined) {
|
|
239
|
+
f.adapters = y.adapters.map(a => ({
|
|
240
|
+
name: a.name,
|
|
241
|
+
...(a.apiUrl ? { apiUrl: a.apiUrl } : {}),
|
|
242
|
+
...(a.description ? { description: a.description } : {}),
|
|
243
|
+
...(a.notes ? { notes: a.notes } : {}),
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
return f;
|
|
247
|
+
}
|
|
59
248
|
|
|
249
|
+
function loadYamlConfig(configPath) {
|
|
60
250
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
251
|
+
if (fs.existsSync(configPath)) {
|
|
252
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
253
|
+
const parsed = yaml.load(content);
|
|
254
|
+
if (parsed && typeof parsed === 'object') {
|
|
255
|
+
return parsed;
|
|
256
|
+
}
|
|
65
257
|
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error(`Error: Failed to parse ${configPath}: ${error.message}`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
66
264
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
265
|
+
// ---- Local asset handling (e.g. header logo file paths) ----
|
|
266
|
+
|
|
267
|
+
function isUrlLike(value) {
|
|
268
|
+
return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(value) || value.startsWith('//');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function resolveLocalAssetPath(rawValue, yamlPath) {
|
|
272
|
+
if (!rawValue || typeof rawValue !== 'string') return null;
|
|
273
|
+
const value = rawValue.trim();
|
|
274
|
+
if (!value || isUrlLike(value)) return null;
|
|
275
|
+
|
|
276
|
+
const expandedValue = value.startsWith('~/')
|
|
277
|
+
? path.join(process.env.HOME || '', value.slice(2))
|
|
278
|
+
: value;
|
|
279
|
+
|
|
280
|
+
const yamlDir = path.dirname(yamlPath);
|
|
281
|
+
const candidates = path.isAbsolute(expandedValue)
|
|
282
|
+
? [expandedValue]
|
|
283
|
+
: [path.resolve(yamlDir, expandedValue), path.resolve(process.cwd(), expandedValue)];
|
|
284
|
+
|
|
285
|
+
for (const candidate of candidates) {
|
|
286
|
+
try {
|
|
287
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
288
|
+
return fs.realpathSync(candidate);
|
|
72
289
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
apiKey: adapter.apiKey || DEFAULT_CONFIG.defaultKey,
|
|
76
|
-
apiUrl: adapter.apiUrl || DEFAULT_CONFIG.apiUrl,
|
|
77
|
-
description: adapter.description || adapter.summary,
|
|
78
|
-
notes: adapter.notes,
|
|
79
|
-
});
|
|
290
|
+
} catch {
|
|
291
|
+
// ignore invalid candidate and continue
|
|
80
292
|
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---- Adapter loading (env secrets + YAML metadata) ----
|
|
299
|
+
|
|
300
|
+
function parseAdaptersFromEnv() {
|
|
301
|
+
const envValue = process.env.ORBIT_ADAPTERS || process.env.VITE_ADAPTERS;
|
|
302
|
+
if (!envValue) return [];
|
|
81
303
|
|
|
82
|
-
|
|
304
|
+
try {
|
|
305
|
+
const parsed = JSON.parse(envValue);
|
|
306
|
+
if (!Array.isArray(parsed)) {
|
|
307
|
+
console.warn('Warning: ORBIT_ADAPTERS/VITE_ADAPTERS must be a JSON array');
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
return parsed.filter(a => a.name).map(a => ({
|
|
311
|
+
name: a.name,
|
|
312
|
+
apiKey: a.apiKey || DEFAULTS.defaultKey,
|
|
313
|
+
apiUrl: a.apiUrl || DEFAULTS.apiUrl,
|
|
314
|
+
description: a.description || a.summary,
|
|
315
|
+
notes: a.notes,
|
|
316
|
+
}));
|
|
83
317
|
} catch (error) {
|
|
84
318
|
console.warn('Warning: Could not parse ORBIT_ADAPTERS/VITE_ADAPTERS:', error.message);
|
|
85
319
|
return [];
|
|
86
320
|
}
|
|
87
321
|
}
|
|
88
322
|
|
|
89
|
-
/**
|
|
90
|
-
* Load adapter mappings from environment variable
|
|
91
|
-
* Format: JSON array of adapter objects
|
|
92
|
-
* Example: ORBIT_ADAPTERS='[{"name":"Simple Chat","apiKey":"key1","apiUrl":"https://api.example.com"}]'
|
|
93
|
-
* @returns {object|null} - Adapters object or null if not found/invalid
|
|
94
|
-
*/
|
|
95
323
|
function loadAdaptersConfig() {
|
|
96
|
-
const adapterList =
|
|
97
|
-
if (adapterList.length === 0)
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
324
|
+
const adapterList = parseAdaptersFromEnv();
|
|
325
|
+
if (adapterList.length === 0) return null;
|
|
100
326
|
|
|
101
327
|
const adapters = {};
|
|
102
328
|
for (const adapter of adapterList) {
|
|
103
329
|
adapters[adapter.name] = {
|
|
104
|
-
apiKey: adapter.apiKey ||
|
|
105
|
-
apiUrl: adapter.apiUrl ||
|
|
330
|
+
apiKey: adapter.apiKey || DEFAULTS.defaultKey,
|
|
331
|
+
apiUrl: adapter.apiUrl || DEFAULTS.apiUrl,
|
|
106
332
|
description: adapter.description,
|
|
107
333
|
notes: adapter.notes,
|
|
108
334
|
};
|
|
@@ -112,113 +338,26 @@ function loadAdaptersConfig() {
|
|
|
112
338
|
}
|
|
113
339
|
|
|
114
340
|
function getDefaultAdapterFromEnv() {
|
|
115
|
-
const adapterList =
|
|
341
|
+
const adapterList = parseAdaptersFromEnv();
|
|
116
342
|
return adapterList.length > 0 ? adapterList[0].name : null;
|
|
117
343
|
}
|
|
118
344
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
*/
|
|
345
|
+
// ---- CLI arg parsing (server flags only) ----
|
|
346
|
+
|
|
122
347
|
function parseArgs() {
|
|
123
348
|
const args = process.argv.slice(2);
|
|
124
|
-
const config = { ...DEFAULT_CONFIG };
|
|
125
349
|
const serverConfig = {
|
|
126
350
|
port: 5173,
|
|
127
351
|
host: 'localhost',
|
|
128
352
|
open: false,
|
|
129
353
|
configFile: null,
|
|
130
354
|
apiOnly: false,
|
|
355
|
+
corsOrigin: undefined,
|
|
131
356
|
};
|
|
132
357
|
|
|
133
358
|
for (let i = 0; i < args.length; i++) {
|
|
134
359
|
const arg = args[i];
|
|
135
|
-
|
|
136
360
|
switch (arg) {
|
|
137
|
-
case '--api-url':
|
|
138
|
-
config.apiUrl = args[++i];
|
|
139
|
-
break;
|
|
140
|
-
case '--api-key':
|
|
141
|
-
config.defaultKey = args[++i];
|
|
142
|
-
break;
|
|
143
|
-
case '--default-adapter':
|
|
144
|
-
config.defaultKey = args[++i];
|
|
145
|
-
break;
|
|
146
|
-
case '--default-key':
|
|
147
|
-
console.warn('Warning: --default-key is deprecated. Use --default-adapter instead.');
|
|
148
|
-
config.defaultKey = args[++i];
|
|
149
|
-
break;
|
|
150
|
-
case '--application-name':
|
|
151
|
-
config.applicationName = args[++i];
|
|
152
|
-
break;
|
|
153
|
-
case '--application-description':
|
|
154
|
-
config.applicationDescription = args[++i];
|
|
155
|
-
break;
|
|
156
|
-
case '--default-input-placeholder':
|
|
157
|
-
config.defaultInputPlaceholder = args[++i];
|
|
158
|
-
break;
|
|
159
|
-
case '--use-local-api':
|
|
160
|
-
config.useLocalApi = true;
|
|
161
|
-
break;
|
|
162
|
-
case '--local-api-path':
|
|
163
|
-
config.localApiPath = args[++i];
|
|
164
|
-
break;
|
|
165
|
-
case '--console-debug':
|
|
166
|
-
config.consoleDebug = true;
|
|
167
|
-
break;
|
|
168
|
-
case '--enable-upload':
|
|
169
|
-
config.enableUploadButton = true;
|
|
170
|
-
break;
|
|
171
|
-
case '--enable-audio':
|
|
172
|
-
config.enableAudioOutput = true;
|
|
173
|
-
break;
|
|
174
|
-
case '--enable-audio-input':
|
|
175
|
-
config.enableAudioInput = true;
|
|
176
|
-
break;
|
|
177
|
-
case '--enable-feedback':
|
|
178
|
-
config.enableFeedbackButtons = true;
|
|
179
|
-
break;
|
|
180
|
-
case '--enable-autocomplete':
|
|
181
|
-
config.enableAutocomplete = true;
|
|
182
|
-
break;
|
|
183
|
-
case '--voice-silence-timeout-ms':
|
|
184
|
-
config.voiceSilenceTimeoutMs = parseInt(args[++i], 10);
|
|
185
|
-
break;
|
|
186
|
-
case '--voice-recognition-lang':
|
|
187
|
-
config.voiceRecognitionLanguage = args[++i];
|
|
188
|
-
break;
|
|
189
|
-
case '--enable-api-middleware':
|
|
190
|
-
// Ignored — middleware mode is now always enabled
|
|
191
|
-
break;
|
|
192
|
-
case '--out-of-service-message':
|
|
193
|
-
config.outOfServiceMessage = args[++i];
|
|
194
|
-
break;
|
|
195
|
-
case '--max-files-per-conversation':
|
|
196
|
-
config.maxFilesPerConversation = parseInt(args[++i], 10);
|
|
197
|
-
break;
|
|
198
|
-
case '--max-file-size-mb':
|
|
199
|
-
config.maxFileSizeMB = parseInt(args[++i], 10);
|
|
200
|
-
break;
|
|
201
|
-
case '--max-total-files':
|
|
202
|
-
config.maxTotalFiles = parseInt(args[++i], 10);
|
|
203
|
-
break;
|
|
204
|
-
case '--max-conversations':
|
|
205
|
-
config.maxConversations = parseInt(args[++i], 10);
|
|
206
|
-
break;
|
|
207
|
-
case '--max-messages-per-conversation':
|
|
208
|
-
config.maxMessagesPerConversation = parseInt(args[++i], 10);
|
|
209
|
-
break;
|
|
210
|
-
case '--max-messages-per-thread':
|
|
211
|
-
config.maxMessagesPerThread = parseInt(args[++i], 10);
|
|
212
|
-
break;
|
|
213
|
-
case '--max-total-messages':
|
|
214
|
-
config.maxTotalMessages = parseInt(args[++i], 10);
|
|
215
|
-
break;
|
|
216
|
-
case '--max-message-length':
|
|
217
|
-
config.maxMessageLength = parseInt(args[++i], 10);
|
|
218
|
-
break;
|
|
219
|
-
case '--settings-about-msg':
|
|
220
|
-
config.settingsAboutMsg = args[++i];
|
|
221
|
-
break;
|
|
222
361
|
case '--port':
|
|
223
362
|
serverConfig.port = parseInt(args[++i], 10);
|
|
224
363
|
break;
|
|
@@ -228,22 +367,19 @@ function parseArgs() {
|
|
|
228
367
|
case '--open':
|
|
229
368
|
serverConfig.open = true;
|
|
230
369
|
break;
|
|
370
|
+
case '--config':
|
|
371
|
+
serverConfig.configFile = args[++i];
|
|
372
|
+
break;
|
|
231
373
|
case '--api-only':
|
|
232
374
|
serverConfig.apiOnly = true;
|
|
233
375
|
break;
|
|
234
376
|
case '--cors-origin':
|
|
235
377
|
serverConfig.corsOrigin = args[++i];
|
|
236
378
|
break;
|
|
237
|
-
case '--config':
|
|
238
|
-
serverConfig.configFile = args[++i];
|
|
239
|
-
break;
|
|
240
379
|
case '--help':
|
|
241
380
|
case '-h':
|
|
242
|
-
// Handled in main() function
|
|
243
|
-
break;
|
|
244
381
|
case '--version':
|
|
245
382
|
case '-v':
|
|
246
|
-
// Handled in main() function
|
|
247
383
|
break;
|
|
248
384
|
default:
|
|
249
385
|
if (arg.startsWith('--')) {
|
|
@@ -254,126 +390,15 @@ function parseArgs() {
|
|
|
254
390
|
}
|
|
255
391
|
}
|
|
256
392
|
|
|
257
|
-
return
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Load configuration from file
|
|
262
|
-
*/
|
|
263
|
-
function loadConfigFile(filePath) {
|
|
264
|
-
try {
|
|
265
|
-
if (fs.existsSync(filePath)) {
|
|
266
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
267
|
-
return JSON.parse(content);
|
|
268
|
-
}
|
|
269
|
-
} catch (error) {
|
|
270
|
-
console.warn(`Warning: Could not load config file ${filePath}:`, error.message);
|
|
271
|
-
}
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Load configuration from environment variables
|
|
277
|
-
*/
|
|
278
|
-
function loadConfigFromEnv() {
|
|
279
|
-
const envConfig = {};
|
|
280
|
-
|
|
281
|
-
// Map VITE_* environment variables to config keys
|
|
282
|
-
const envMap = {
|
|
283
|
-
VITE_API_URL: 'apiUrl',
|
|
284
|
-
VITE_DEFAULT_KEY: 'defaultKey',
|
|
285
|
-
VITE_APPLICATION_NAME: 'applicationName',
|
|
286
|
-
VITE_APPLICATION_DESCRIPTION: 'applicationDescription',
|
|
287
|
-
VITE_DEFAULT_INPUT_PLACEHOLDER: 'defaultInputPlaceholder',
|
|
288
|
-
VITE_USE_LOCAL_API: 'useLocalApi',
|
|
289
|
-
VITE_LOCAL_API_PATH: 'localApiPath',
|
|
290
|
-
VITE_CONSOLE_DEBUG: 'consoleDebug',
|
|
291
|
-
VITE_ENABLE_UPLOAD: 'enableUploadButton',
|
|
292
|
-
VITE_ENABLE_AUDIO_OUTPUT: 'enableAudioOutput',
|
|
293
|
-
VITE_ENABLE_AUDIO_INPUT: 'enableAudioInput',
|
|
294
|
-
VITE_ENABLE_FEEDBACK: 'enableFeedbackButtons',
|
|
295
|
-
VITE_ENABLE_AUTOCOMPLETE: 'enableAutocomplete',
|
|
296
|
-
VITE_VOICE_SILENCE_TIMEOUT_MS: 'voiceSilenceTimeoutMs',
|
|
297
|
-
VITE_VOICE_RECOGNITION_LANG: 'voiceRecognitionLanguage',
|
|
298
|
-
VITE_OUT_OF_SERVICE_MESSAGE: 'outOfServiceMessage',
|
|
299
|
-
VITE_MAX_FILES_PER_CONVERSATION: 'maxFilesPerConversation',
|
|
300
|
-
VITE_MAX_FILE_SIZE_MB: 'maxFileSizeMB',
|
|
301
|
-
VITE_MAX_TOTAL_FILES: 'maxTotalFiles',
|
|
302
|
-
VITE_MAX_CONVERSATIONS: 'maxConversations',
|
|
303
|
-
VITE_MAX_MESSAGES_PER_CONVERSATION: 'maxMessagesPerConversation',
|
|
304
|
-
VITE_MAX_MESSAGES_PER_THREAD: 'maxMessagesPerThread',
|
|
305
|
-
VITE_MAX_TOTAL_MESSAGES: 'maxTotalMessages',
|
|
306
|
-
VITE_MAX_MESSAGE_LENGTH: 'maxMessageLength',
|
|
307
|
-
VITE_SETTINGS_ABOUT_MSG: 'settingsAboutMsg',
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
for (const [envKey, configKey] of Object.entries(envMap)) {
|
|
311
|
-
const value = process.env[envKey];
|
|
312
|
-
if (value !== undefined) {
|
|
313
|
-
if (configKey === 'useLocalApi' || configKey === 'consoleDebug' ||
|
|
314
|
-
configKey === 'enableUploadButton' || configKey === 'enableAudioOutput' ||
|
|
315
|
-
configKey === 'enableAudioInput' || configKey === 'enableFeedbackButtons' || configKey === 'enableAutocomplete') {
|
|
316
|
-
envConfig[configKey] = value === 'true';
|
|
317
|
-
} else if (configKey === 'voiceSilenceTimeoutMs') {
|
|
318
|
-
const parsed = parseInt(value, 10);
|
|
319
|
-
if (!isNaN(parsed)) {
|
|
320
|
-
envConfig[configKey] = parsed;
|
|
321
|
-
}
|
|
322
|
-
} else if (configKey.includes('max') && configKey !== 'maxFileSizeMB') {
|
|
323
|
-
const parsed = parseInt(value, 10);
|
|
324
|
-
if (!isNaN(parsed)) {
|
|
325
|
-
envConfig[configKey] = parsed;
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
envConfig[configKey] = value;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return envConfig;
|
|
393
|
+
return serverConfig;
|
|
334
394
|
}
|
|
335
395
|
|
|
336
|
-
|
|
337
|
-
* Merge configurations in priority order: CLI args > config file > env vars > defaults
|
|
338
|
-
* Note: GitHub stats/owner/repo are not included in runtime config - they're hardcoded
|
|
339
|
-
* and only configurable via build-time env vars for developers who fork.
|
|
340
|
-
*/
|
|
341
|
-
function mergeConfig(cliConfig, serverConfig) {
|
|
342
|
-
// Start with defaults
|
|
343
|
-
let config = { ...DEFAULT_CONFIG };
|
|
344
|
-
|
|
345
|
-
// Load from environment variables (excluding GitHub config)
|
|
346
|
-
const envConfig = loadConfigFromEnv();
|
|
347
|
-
config = { ...config, ...envConfig };
|
|
348
|
-
|
|
349
|
-
// Load from config file (excluding GitHub config)
|
|
350
|
-
const configDir = path.join(homedir(), '.orbit-chat-app');
|
|
351
|
-
const defaultConfigFile = path.join(configDir, 'config.json');
|
|
352
|
-
const configFile = serverConfig.configFile || defaultConfigFile;
|
|
353
|
-
|
|
354
|
-
const fileConfig = loadConfigFile(configFile);
|
|
355
|
-
if (fileConfig) {
|
|
356
|
-
// Remove GitHub config from file config if present
|
|
357
|
-
const { showGitHubStats, githubOwner, githubRepo, ...fileConfigWithoutGitHub } = fileConfig;
|
|
358
|
-
config = { ...config, ...fileConfigWithoutGitHub };
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// CLI arguments override everything (excluding GitHub config)
|
|
362
|
-
const { showGitHubStats, githubOwner, githubRepo, ...cliConfigWithoutGitHub } = cliConfig;
|
|
363
|
-
config = { ...config, ...cliConfigWithoutGitHub };
|
|
364
|
-
|
|
365
|
-
return config;
|
|
366
|
-
}
|
|
396
|
+
// ---- HTML injection ----
|
|
367
397
|
|
|
368
|
-
/**
|
|
369
|
-
* Inject configuration into HTML
|
|
370
|
-
* The config script MUST be placed in <head> BEFORE the main JS module loads,
|
|
371
|
-
* otherwise window.ORBIT_CHAT_CONFIG won't be available when the app initializes.
|
|
372
|
-
*/
|
|
373
398
|
function injectConfig(html, config) {
|
|
374
399
|
const configScript = `<script>window.ORBIT_CHAT_CONFIG = ${JSON.stringify(config)};</script>`;
|
|
375
400
|
|
|
376
|
-
// Remove the placeholder script tag
|
|
401
|
+
// Remove the placeholder script tag
|
|
377
402
|
html = html.replace(
|
|
378
403
|
/<script id="orbit-chat-config" type="application\/json">[\s\S]*?<\/script>/,
|
|
379
404
|
'<!-- Config injected in head -->'
|
|
@@ -385,34 +410,87 @@ function injectConfig(html, config) {
|
|
|
385
410
|
/<title>.*?<\/title>/i,
|
|
386
411
|
`<title>${config.applicationName}</title>`
|
|
387
412
|
);
|
|
388
|
-
// Also update apple-mobile-web-app-title meta tag
|
|
389
413
|
html = html.replace(
|
|
390
414
|
/<meta name="apple-mobile-web-app-title" content="[^"]*" \/>/i,
|
|
391
415
|
`<meta name="apple-mobile-web-app-title" content="${config.applicationName}" />`
|
|
392
416
|
);
|
|
393
417
|
}
|
|
394
418
|
|
|
395
|
-
// Inject the config script at the START of <head
|
|
396
|
-
// This ensures window.ORBIT_CHAT_CONFIG is available when the main JS bundle loads
|
|
419
|
+
// Inject the config script at the START of <head>
|
|
397
420
|
return html.replace(
|
|
398
421
|
/<head>/i,
|
|
399
422
|
'<head>\n ' + configScript
|
|
400
423
|
);
|
|
401
424
|
}
|
|
402
425
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
426
|
+
// ---- Rate limiting ----
|
|
427
|
+
|
|
428
|
+
function createRateLimiters(rateLimitConfig) {
|
|
429
|
+
if (!rateLimitConfig || rateLimitConfig.enabled === false) return null;
|
|
430
|
+
|
|
431
|
+
const windowMs = rateLimitConfig.windowMs || 60000;
|
|
432
|
+
const maxRequests = rateLimitConfig.maxRequests || 30;
|
|
433
|
+
const chatWindowMs = rateLimitConfig.chat?.windowMs || 60000;
|
|
434
|
+
const chatMaxRequests = rateLimitConfig.chat?.maxRequests || 10;
|
|
435
|
+
|
|
436
|
+
const keyGenerator = (req) =>
|
|
437
|
+
req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
438
|
+
|
|
439
|
+
const api = rateLimit({
|
|
440
|
+
windowMs,
|
|
441
|
+
max: maxRequests,
|
|
442
|
+
keyGenerator,
|
|
443
|
+
standardHeaders: 'draft-7',
|
|
444
|
+
legacyHeaders: false,
|
|
445
|
+
validate: { default: true, keyGeneratorIpFallback: false },
|
|
446
|
+
skip: (req) => req.method === 'OPTIONS' || req.path === '/adapters',
|
|
447
|
+
handler: (req, res) => {
|
|
448
|
+
const retryAfterMs = res.getHeader('RateLimit-Reset')
|
|
449
|
+
? Number(res.getHeader('RateLimit-Reset')) * 1000
|
|
450
|
+
: windowMs;
|
|
451
|
+
res.status(429).json({
|
|
452
|
+
error: 'Too many requests',
|
|
453
|
+
message: `Rate limit exceeded. Try again in ${Math.ceil(retryAfterMs / 1000)} seconds.`,
|
|
454
|
+
retryAfterMs,
|
|
455
|
+
});
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const chat = rateLimit({
|
|
460
|
+
windowMs: chatWindowMs,
|
|
461
|
+
max: chatMaxRequests,
|
|
462
|
+
keyGenerator,
|
|
463
|
+
standardHeaders: 'draft-7',
|
|
464
|
+
legacyHeaders: false,
|
|
465
|
+
validate: { default: true, keyGeneratorIpFallback: false },
|
|
466
|
+
handler: (req, res) => {
|
|
467
|
+
const retryAfterMs = res.getHeader('RateLimit-Reset')
|
|
468
|
+
? Number(res.getHeader('RateLimit-Reset')) * 1000
|
|
469
|
+
: chatWindowMs;
|
|
470
|
+
res.status(429).json({
|
|
471
|
+
error: 'Too many requests',
|
|
472
|
+
message: `Chat rate limit exceeded. Try again in ${Math.ceil(retryAfterMs / 1000)} seconds.`,
|
|
473
|
+
retryAfterMs,
|
|
474
|
+
});
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return { api, chat };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ---- Express server ----
|
|
482
|
+
|
|
410
483
|
function createServer(distPath, config, serverConfig = {}) {
|
|
411
484
|
const app = express();
|
|
412
485
|
const adapters = loadAdaptersConfig();
|
|
413
486
|
const apiOnly = serverConfig.apiOnly || false;
|
|
487
|
+
const localAssets = serverConfig.localAssets || {};
|
|
488
|
+
const yamlAdapterMetadata = new Map(
|
|
489
|
+
Array.isArray(config.adapters)
|
|
490
|
+
? config.adapters.filter(a => a && a.name).map(a => [a.name, a])
|
|
491
|
+
: []
|
|
492
|
+
);
|
|
414
493
|
|
|
415
|
-
// In api-only mode, add CORS middleware so external UIs on other origins can call the API
|
|
416
494
|
if (apiOnly) {
|
|
417
495
|
const allowedOrigin = serverConfig.corsOrigin || '*';
|
|
418
496
|
app.use((req, res, next) => {
|
|
@@ -427,10 +505,57 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
427
505
|
});
|
|
428
506
|
}
|
|
429
507
|
|
|
508
|
+
if (Object.keys(localAssets).length > 0) {
|
|
509
|
+
app.get('/__orbitchat_assets/:assetId', (req, res) => {
|
|
510
|
+
const assetPath = localAssets[req.params.assetId];
|
|
511
|
+
if (!assetPath) {
|
|
512
|
+
return res.status(404).send('Asset not found');
|
|
513
|
+
}
|
|
514
|
+
if (!fs.existsSync(assetPath)) {
|
|
515
|
+
return res.status(404).send('Asset not found');
|
|
516
|
+
}
|
|
517
|
+
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
518
|
+
res.sendFile(assetPath, (error) => {
|
|
519
|
+
if (error && !res.headersSent) {
|
|
520
|
+
res.status(500).send('Failed to serve asset');
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Guest rate limiting — after CORS, before proxy. Skips authenticated requests.
|
|
527
|
+
const limiters = createRateLimiters(serverConfig.rateLimit);
|
|
528
|
+
if (limiters) {
|
|
529
|
+
app.use('/api', (req, res, next) => {
|
|
530
|
+
if (req.headers.authorization) return next();
|
|
531
|
+
limiters.api(req, res, next);
|
|
532
|
+
});
|
|
533
|
+
app.use('/api', (req, res, next) => {
|
|
534
|
+
if (req.headers.authorization) return next();
|
|
535
|
+
if (req.method === 'POST' && (/\/chat/i.test(req.path) || /\/stream/i.test(req.path))) {
|
|
536
|
+
return limiters.chat(req, res, next);
|
|
537
|
+
}
|
|
538
|
+
next();
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
430
542
|
// API proxy endpoints - MUST be before body parsers
|
|
431
543
|
if (adapters) {
|
|
432
|
-
//
|
|
433
|
-
|
|
544
|
+
// Merge adapter metadata from YAML so UI labels/notes are consistent with dev mode.
|
|
545
|
+
for (const [adapterName, adapter] of Object.entries(adapters)) {
|
|
546
|
+
const metadata = yamlAdapterMetadata.get(adapterName);
|
|
547
|
+
if (!metadata) continue;
|
|
548
|
+
if (!adapter.description && metadata.description) {
|
|
549
|
+
adapter.description = metadata.description;
|
|
550
|
+
}
|
|
551
|
+
if (!adapter.notes && metadata.notes) {
|
|
552
|
+
adapter.notes = metadata.notes;
|
|
553
|
+
}
|
|
554
|
+
if (!adapter.apiUrl && metadata.apiUrl) {
|
|
555
|
+
adapter.apiUrl = metadata.apiUrl;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
434
559
|
const proxyInstances = {};
|
|
435
560
|
for (const [adapterName, adapter] of Object.entries(adapters)) {
|
|
436
561
|
if (!adapter.apiKey || !adapter.apiUrl) {
|
|
@@ -440,33 +565,26 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
440
565
|
proxyInstances[adapterName] = createProxyMiddleware({
|
|
441
566
|
target: adapter.apiUrl,
|
|
442
567
|
changeOrigin: true,
|
|
443
|
-
// Restore /api prefix for backend paths that need it (files, threads)
|
|
444
568
|
pathRewrite: (path) => {
|
|
445
569
|
if (path.startsWith('/files') || path.startsWith('/threads')) {
|
|
446
570
|
return '/api' + path;
|
|
447
571
|
}
|
|
448
572
|
return path;
|
|
449
573
|
},
|
|
450
|
-
// Set headers directly - this is more reliable than onProxyReq for some cases
|
|
451
574
|
headers: {
|
|
452
575
|
'X-API-Key': adapter.apiKey,
|
|
453
576
|
},
|
|
454
|
-
// Critical for SSE streaming - disable response buffering
|
|
455
577
|
selfHandleResponse: false,
|
|
456
578
|
onProxyReq: (proxyReq, req) => {
|
|
457
|
-
// Remove adapter name header
|
|
458
579
|
proxyReq.removeHeader('x-adapter-name');
|
|
459
|
-
// Ensure API key is set (backup to headers option above)
|
|
460
580
|
proxyReq.setHeader('X-API-Key', adapter.apiKey);
|
|
461
|
-
|
|
462
|
-
const headersToPreserve = ['content-type', 'x-session-id', 'x-thread-id', 'accept', 'content-length'];
|
|
581
|
+
const headersToPreserve = ['content-type', 'x-session-id', 'x-thread-id', 'accept', 'content-length', 'authorization'];
|
|
463
582
|
headersToPreserve.forEach(header => {
|
|
464
583
|
const value = req.headers[header];
|
|
465
584
|
if (value) {
|
|
466
585
|
proxyReq.setHeader(header, value);
|
|
467
586
|
}
|
|
468
587
|
});
|
|
469
|
-
// Copy all other headers
|
|
470
588
|
Object.keys(req.headers).forEach(key => {
|
|
471
589
|
const lowerKey = key.toLowerCase();
|
|
472
590
|
if (!['x-adapter-name', 'host', 'connection', 'transfer-encoding'].includes(lowerKey)) {
|
|
@@ -478,18 +596,14 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
478
596
|
});
|
|
479
597
|
},
|
|
480
598
|
onProxyRes: (proxyRes, req, res) => {
|
|
481
|
-
// Handle CORS if needed
|
|
482
599
|
proxyRes.headers['access-control-allow-origin'] = '*';
|
|
483
600
|
proxyRes.headers['access-control-allow-methods'] = 'GET, POST, PUT, DELETE, OPTIONS';
|
|
484
601
|
proxyRes.headers['access-control-allow-headers'] = 'Content-Type, X-API-Key, X-Session-ID, X-Thread-ID, X-Adapter-Name';
|
|
485
602
|
|
|
486
|
-
// Critical for SSE streaming - disable buffering
|
|
487
603
|
const contentType = proxyRes.headers['content-type'] || '';
|
|
488
604
|
if (contentType.includes('text/event-stream')) {
|
|
489
|
-
// Disable caching and buffering for SSE
|
|
490
605
|
proxyRes.headers['cache-control'] = 'no-cache';
|
|
491
606
|
proxyRes.headers['x-accel-buffering'] = 'no';
|
|
492
|
-
// Flush response immediately
|
|
493
607
|
if (res.flushHeaders) {
|
|
494
608
|
res.flushHeaders();
|
|
495
609
|
}
|
|
@@ -501,25 +615,62 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
501
615
|
res.status(500).json({ error: 'Proxy error', message: err.message });
|
|
502
616
|
}
|
|
503
617
|
},
|
|
504
|
-
ws: false,
|
|
505
|
-
logLevel: 'silent',
|
|
618
|
+
ws: false,
|
|
619
|
+
logLevel: 'silent',
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Fetch model info from ORBIT backend for each adapter (lazy, short-lived cache).
|
|
624
|
+
let modelsLastFetchedAt = 0;
|
|
625
|
+
let modelsFetchInFlight = null;
|
|
626
|
+
const MODELS_CACHE_TTL_MS = 30000;
|
|
627
|
+
async function fetchAdapterModels(force = false) {
|
|
628
|
+
const now = Date.now();
|
|
629
|
+
if (!force && (now - modelsLastFetchedAt) < MODELS_CACHE_TTL_MS) return;
|
|
630
|
+
if (modelsFetchInFlight) return modelsFetchInFlight;
|
|
631
|
+
|
|
632
|
+
modelsFetchInFlight = (async () => {
|
|
633
|
+
const fetches = Object.entries(adapters).map(async ([, adapter]) => {
|
|
634
|
+
if (!adapter.apiUrl || !adapter.apiKey) return;
|
|
635
|
+
try {
|
|
636
|
+
const url = `${adapter.apiUrl.replace(/\/+$/, '')}/admin/adapters/info`;
|
|
637
|
+
const resp = await fetch(url, {
|
|
638
|
+
headers: { 'X-API-Key': adapter.apiKey },
|
|
639
|
+
signal: AbortSignal.timeout(5000),
|
|
640
|
+
});
|
|
641
|
+
if (resp.ok) {
|
|
642
|
+
const info = await resp.json();
|
|
643
|
+
const model = typeof info?.model === 'string' ? info.model.trim() : '';
|
|
644
|
+
adapter.model = model || null;
|
|
645
|
+
}
|
|
646
|
+
} catch {
|
|
647
|
+
// Silently ignore — model will be omitted
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
await Promise.all(fetches);
|
|
651
|
+
modelsLastFetchedAt = Date.now();
|
|
652
|
+
})().finally(() => {
|
|
653
|
+
modelsFetchInFlight = null;
|
|
506
654
|
});
|
|
655
|
+
|
|
656
|
+
return modelsFetchInFlight;
|
|
507
657
|
}
|
|
508
658
|
|
|
509
|
-
|
|
510
|
-
|
|
659
|
+
app.get('/api/adapters', async (req, res) => {
|
|
660
|
+
const cacheControl = typeof req.headers['cache-control'] === 'string' ? req.headers['cache-control'] : '';
|
|
661
|
+
const forceRefresh = req.query?.refresh === '1' || cacheControl.includes('no-cache');
|
|
662
|
+
await fetchAdapterModels(forceRefresh);
|
|
511
663
|
const adapterList = Object.keys(adapters).map(name => ({
|
|
512
664
|
name,
|
|
513
665
|
description: adapters[name]?.description,
|
|
514
666
|
notes: adapters[name]?.notes,
|
|
667
|
+
model: adapters[name]?.model || null,
|
|
515
668
|
}));
|
|
669
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
516
670
|
res.json({ adapters: adapterList });
|
|
517
671
|
});
|
|
518
672
|
|
|
519
|
-
// Proxy middleware for API requests - must be before body parsers to preserve request stream
|
|
520
|
-
// Note: Uses /api path instead of /api/proxy for security (hides proxy nature)
|
|
521
673
|
app.use('/api', (req, res, next) => {
|
|
522
|
-
// Skip the /api/adapters route - it's handled separately above
|
|
523
674
|
if (req.path === '/adapters') {
|
|
524
675
|
return next('route');
|
|
525
676
|
}
|
|
@@ -539,14 +690,10 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
539
690
|
});
|
|
540
691
|
}
|
|
541
692
|
|
|
542
|
-
// Middleware for parsing JSON - after proxy routes to preserve request body stream
|
|
543
693
|
app.use(express.json());
|
|
544
694
|
app.use(express.urlencoded({ extended: true }));
|
|
545
695
|
|
|
546
|
-
// --- UI serving (skipped in api-only mode) ---
|
|
547
696
|
if (!apiOnly && distPath) {
|
|
548
|
-
// IMPORTANT: Handle index.html BEFORE express.static to inject runtime config
|
|
549
|
-
// express.static would serve the file without config injection otherwise
|
|
550
697
|
app.get(['/', '/index.html'], (req, res) => {
|
|
551
698
|
try {
|
|
552
699
|
const indexPath = path.join(distPath, 'index.html');
|
|
@@ -560,24 +707,15 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
560
707
|
}
|
|
561
708
|
});
|
|
562
709
|
|
|
563
|
-
|
|
564
|
-
app.use(express.static(distPath, {
|
|
565
|
-
index: false, // Don't serve index.html automatically - we handle it above
|
|
566
|
-
}));
|
|
710
|
+
app.use(express.static(distPath, { index: false }));
|
|
567
711
|
|
|
568
|
-
// SPA fallback - serve index.html for all non-file routes (client-side routing)
|
|
569
712
|
app.get('/{*splat}', (req, res, next) => {
|
|
570
|
-
// Skip API routes
|
|
571
713
|
if (req.path.startsWith('/api/')) {
|
|
572
714
|
return next();
|
|
573
715
|
}
|
|
574
|
-
|
|
575
|
-
// Skip requests for files with extensions (let them 404)
|
|
576
716
|
if (path.extname(req.path)) {
|
|
577
717
|
return res.status(404).send('Not Found');
|
|
578
718
|
}
|
|
579
|
-
|
|
580
|
-
// Serve index.html for SPA routes
|
|
581
719
|
try {
|
|
582
720
|
const indexPath = path.join(distPath, 'index.html');
|
|
583
721
|
let content = fs.readFileSync(indexPath, 'utf8');
|
|
@@ -594,173 +732,116 @@ function createServer(distPath, config, serverConfig = {}) {
|
|
|
594
732
|
return app;
|
|
595
733
|
}
|
|
596
734
|
|
|
597
|
-
|
|
598
|
-
* Get MIME type for file extension
|
|
599
|
-
*/
|
|
600
|
-
function getMimeType(filePath) {
|
|
601
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
602
|
-
const mimeTypes = {
|
|
603
|
-
'.html': 'text/html',
|
|
604
|
-
'.js': 'application/javascript',
|
|
605
|
-
'.mjs': 'application/javascript',
|
|
606
|
-
'.json': 'application/json',
|
|
607
|
-
'.css': 'text/css',
|
|
608
|
-
'.png': 'image/png',
|
|
609
|
-
'.jpg': 'image/jpeg',
|
|
610
|
-
'.jpeg': 'image/jpeg',
|
|
611
|
-
'.gif': 'image/gif',
|
|
612
|
-
'.svg': 'image/svg+xml',
|
|
613
|
-
'.ico': 'image/x-icon',
|
|
614
|
-
'.woff': 'font/woff',
|
|
615
|
-
'.woff2': 'font/woff2',
|
|
616
|
-
'.ttf': 'font/ttf',
|
|
617
|
-
'.eot': 'application/vnd.ms-fontobject',
|
|
618
|
-
};
|
|
619
|
-
return mimeTypes[ext] || 'application/octet-stream';
|
|
620
|
-
}
|
|
735
|
+
// ---- Utilities ----
|
|
621
736
|
|
|
622
|
-
/**
|
|
623
|
-
* Open browser
|
|
624
|
-
*/
|
|
625
737
|
function openBrowser(url) {
|
|
626
738
|
const platform = process.platform;
|
|
627
739
|
let command;
|
|
628
|
-
|
|
629
|
-
if (platform === '
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
command = `xdg-open "${url}"`;
|
|
633
|
-
} else if (platform === 'win32') {
|
|
634
|
-
command = `start "${url}"`;
|
|
635
|
-
} else {
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
740
|
+
if (platform === 'darwin') command = `open "${url}"`;
|
|
741
|
+
else if (platform === 'linux') command = `xdg-open "${url}"`;
|
|
742
|
+
else if (platform === 'win32') command = `start "${url}"`;
|
|
743
|
+
else return;
|
|
638
744
|
|
|
639
|
-
try {
|
|
640
|
-
execSync(command, { stdio: 'ignore' });
|
|
641
|
-
} catch (error) {
|
|
642
|
-
// Ignore errors
|
|
643
|
-
}
|
|
745
|
+
try { execSync(command, { stdio: 'ignore' }); } catch { /* ignore */ }
|
|
644
746
|
}
|
|
645
747
|
|
|
646
|
-
/**
|
|
647
|
-
* Get version from package.json
|
|
648
|
-
*/
|
|
649
748
|
function getVersion() {
|
|
650
749
|
try {
|
|
651
750
|
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
652
751
|
const packageContent = fs.readFileSync(packagePath, 'utf8');
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
} catch (error) {
|
|
656
|
-
return 'unknown';
|
|
657
|
-
}
|
|
752
|
+
return JSON.parse(packageContent).version || 'unknown';
|
|
753
|
+
} catch { return 'unknown'; }
|
|
658
754
|
}
|
|
659
755
|
|
|
660
|
-
/**
|
|
661
|
-
* Print version
|
|
662
|
-
*/
|
|
663
|
-
function printVersion() {
|
|
664
|
-
console.log(getVersion());
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* Print help message
|
|
669
|
-
*/
|
|
670
756
|
function printHelp() {
|
|
671
757
|
console.log(`
|
|
672
758
|
ORBIT Chat CLI
|
|
673
759
|
|
|
674
760
|
Usage: orbitchat [options]
|
|
675
761
|
|
|
762
|
+
All application settings are configured in orbitchat.yaml (see orbitchat.yaml.example).
|
|
763
|
+
Secrets (adapter API keys) go in VITE_ADAPTERS / ORBIT_ADAPTERS env var.
|
|
764
|
+
|
|
676
765
|
Options:
|
|
677
|
-
--
|
|
678
|
-
--
|
|
679
|
-
--
|
|
680
|
-
--
|
|
681
|
-
--
|
|
682
|
-
--
|
|
683
|
-
--
|
|
684
|
-
--
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
--enable-feedback Enable feedback buttons (default: false)
|
|
690
|
-
--enable-autocomplete Enable autocomplete suggestions (default: false)
|
|
691
|
-
--voice-silence-timeout-ms N Auto-stop voice capture after N ms of silence (default: 4000)
|
|
692
|
-
--voice-recognition-lang LANG BCP-47 language code for speech recognition (default: browser locale)
|
|
693
|
-
--enable-api-middleware Ignored (middleware mode is always enabled)
|
|
694
|
-
--out-of-service-message TEXT Show maintenance screen blocking access
|
|
695
|
-
--max-files-per-conversation N Max files per conversation (default: 5)
|
|
696
|
-
--max-file-size-mb N Max file size in MB (default: 50)
|
|
697
|
-
--max-total-files N Max total files (default: 100, 0 = unlimited)
|
|
698
|
-
--max-conversations N Max conversations (default: 10, 0 = unlimited)
|
|
699
|
-
--max-messages-per-conversation N Max messages per conversation (default: 1000, 0 = unlimited)
|
|
700
|
-
--max-messages-per-thread N Max messages per thread (default: 1000, 0 = unlimited)
|
|
701
|
-
--max-total-messages N Max total messages (default: 10000, 0 = unlimited)
|
|
702
|
-
--max-message-length N Max message length (default: 1000)
|
|
703
|
-
--settings-about-msg TEXT About message in settings page (default: ORBIT Chat)
|
|
704
|
-
--port PORT Server port (default: 5173)
|
|
705
|
-
--host HOST Server host (default: localhost)
|
|
706
|
-
--open Open browser automatically
|
|
707
|
-
--api-only Run API proxy only (no UI serving, no build required)
|
|
708
|
-
--cors-origin ORIGIN Allowed CORS origin in api-only mode (default: *)
|
|
709
|
-
--config PATH Path to config file (default: ~/.orbit-chat-app/config.json)
|
|
710
|
-
--help, -h Show this help message
|
|
711
|
-
--version, -v Show version number
|
|
712
|
-
|
|
713
|
-
Configuration Priority:
|
|
714
|
-
1. CLI arguments
|
|
715
|
-
2. Config file (~/.orbit-chat-app/config.json)
|
|
716
|
-
3. Environment variables (VITE_*)
|
|
717
|
-
4. Default values
|
|
718
|
-
|
|
719
|
-
Environment Variables for Middleware Mode:
|
|
720
|
-
ORBIT_ADAPTERS or VITE_ADAPTERS JSON array of adapter configurations
|
|
721
|
-
Example: '[{"name":"Chat","apiKey":"key1","apiUrl":"https://api.example.com"}]'
|
|
766
|
+
--port PORT Server port (default: 5173)
|
|
767
|
+
--host HOST Server host (default: localhost)
|
|
768
|
+
--open Open browser automatically
|
|
769
|
+
--config PATH Path to orbitchat.yaml (default: ./orbitchat.yaml)
|
|
770
|
+
--api-only Run API proxy only (no UI serving)
|
|
771
|
+
--cors-origin URL Allowed CORS origin in api-only mode (default: *)
|
|
772
|
+
--help, -h Show this help message
|
|
773
|
+
--version, -v Show version number
|
|
774
|
+
|
|
775
|
+
Environment Variables:
|
|
776
|
+
ORBIT_ADAPTERS or VITE_ADAPTERS JSON array of adapter configurations (secrets)
|
|
777
|
+
Example: '[{"name":"Chat","apiKey":"key1","apiUrl":"https://api.example.com"}]'
|
|
722
778
|
|
|
723
779
|
Examples:
|
|
724
|
-
orbitchat --
|
|
725
|
-
orbitchat --
|
|
726
|
-
orbitchat --enable-audio --enable-audio-input --enable-upload --console-debug
|
|
727
|
-
orbitchat --config /path/to/config.json
|
|
728
|
-
ORBIT_ADAPTERS='[{"name":"Chat","apiKey":"mykey","apiUrl":"https://api.example.com"}]' orbitchat
|
|
729
|
-
orbitchat --api-only --port 5174
|
|
780
|
+
orbitchat --port 8080
|
|
781
|
+
orbitchat --config /path/to/orbitchat.yaml --open
|
|
730
782
|
orbitchat --api-only --cors-origin http://localhost:3001
|
|
731
783
|
`);
|
|
732
|
-
|
|
733
784
|
}
|
|
734
785
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
*/
|
|
786
|
+
// ---- Main ----
|
|
787
|
+
|
|
738
788
|
function main() {
|
|
739
|
-
// Check for version flag first
|
|
740
789
|
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
741
|
-
|
|
790
|
+
console.log(getVersion());
|
|
742
791
|
return;
|
|
743
792
|
}
|
|
744
|
-
|
|
745
|
-
// Check for help flag
|
|
746
793
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
747
794
|
printHelp();
|
|
748
795
|
return;
|
|
749
796
|
}
|
|
750
797
|
|
|
751
|
-
const
|
|
752
|
-
|
|
798
|
+
const serverConfig = parseArgs();
|
|
799
|
+
loadDotEnv(process.cwd());
|
|
800
|
+
|
|
801
|
+
// Load YAML config
|
|
802
|
+
const yamlPath = serverConfig.configFile || path.join(process.cwd(), 'orbitchat.yaml');
|
|
803
|
+
const yamlObj = loadYamlConfig(yamlPath);
|
|
804
|
+
const yamlFlat = yamlObj ? flattenYamlConfig(yamlObj) : {};
|
|
753
805
|
|
|
806
|
+
if (yamlObj) {
|
|
807
|
+
console.debug(`Loaded config from ${yamlPath}`);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Merge: DEFAULTS < YAML config < auth secrets from env
|
|
811
|
+
const config = { ...DEFAULTS, ...yamlFlat };
|
|
812
|
+
const localAssets = {};
|
|
813
|
+
|
|
814
|
+
// Support local filesystem paths for header logo URL.
|
|
815
|
+
const resolvedHeaderLogoPath = resolveLocalAssetPath(config.headerLogoUrl, yamlPath);
|
|
816
|
+
if (resolvedHeaderLogoPath) {
|
|
817
|
+
const mtimeMs = fs.statSync(resolvedHeaderLogoPath).mtimeMs || Date.now();
|
|
818
|
+
localAssets.header_logo = resolvedHeaderLogoPath;
|
|
819
|
+
config.headerLogoUrl = `/__orbitchat_assets/header_logo?v=${Math.floor(mtimeMs)}`;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Auth secrets from env
|
|
823
|
+
if (process.env.VITE_AUTH_DOMAIN) config.authDomain = process.env.VITE_AUTH_DOMAIN;
|
|
824
|
+
if (process.env.VITE_AUTH_CLIENT_ID) config.authClientId = process.env.VITE_AUTH_CLIENT_ID;
|
|
825
|
+
if (process.env.VITE_AUTH_AUDIENCE) config.authAudience = process.env.VITE_AUTH_AUDIENCE;
|
|
826
|
+
|
|
827
|
+
// Default adapter fallback
|
|
754
828
|
const trimmedDefaultKey = (config.defaultKey || '').trim();
|
|
755
|
-
if (!trimmedDefaultKey || trimmedDefaultKey ===
|
|
829
|
+
if (!trimmedDefaultKey || trimmedDefaultKey === DEFAULTS.defaultKey) {
|
|
756
830
|
const fallbackAdapter = getDefaultAdapterFromEnv();
|
|
757
831
|
if (fallbackAdapter) {
|
|
758
832
|
config.defaultKey = fallbackAdapter;
|
|
759
|
-
console.debug(`ℹ️ Using '${fallbackAdapter}' as the default adapter (first entry from VITE_ADAPTERS).`);
|
|
760
833
|
}
|
|
761
834
|
}
|
|
762
835
|
|
|
763
|
-
//
|
|
836
|
+
// Guest rate limiting (server-only, never sent to browser)
|
|
837
|
+
if (yamlObj && yamlObj.guestLimits?.rateLimit) {
|
|
838
|
+
serverConfig.rateLimit = yamlObj.guestLimits.rateLimit;
|
|
839
|
+
}
|
|
840
|
+
if (Object.keys(localAssets).length > 0) {
|
|
841
|
+
serverConfig.localAssets = localAssets;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Find dist directory
|
|
764
845
|
const distPath = path.join(__dirname, '..', 'dist');
|
|
765
846
|
|
|
766
847
|
if (!serverConfig.apiOnly && !fs.existsSync(distPath)) {
|
|
@@ -768,7 +849,6 @@ function main() {
|
|
|
768
849
|
process.exit(1);
|
|
769
850
|
}
|
|
770
851
|
|
|
771
|
-
// Create and start server
|
|
772
852
|
const app = createServer(
|
|
773
853
|
serverConfig.apiOnly ? null : distPath,
|
|
774
854
|
config,
|
|
@@ -785,9 +865,18 @@ function main() {
|
|
|
785
865
|
console.debug('Configuration:');
|
|
786
866
|
console.debug(` Mode: ${serverConfig.apiOnly ? 'API-only (no UI)' : 'Full (API + UI)'}`);
|
|
787
867
|
console.debug(` API URL: ${config.apiUrl}`);
|
|
788
|
-
console.debug(` Default
|
|
868
|
+
console.debug(` Default Adapter: ${config.defaultKey || '(not set)'}`);
|
|
789
869
|
console.debug(` Port: ${serverConfig.port}`);
|
|
790
870
|
console.debug(` Host: ${serverConfig.host}`);
|
|
871
|
+
if (yamlObj) {
|
|
872
|
+
console.debug(` Config: ${yamlPath}`);
|
|
873
|
+
}
|
|
874
|
+
if (resolvedHeaderLogoPath) {
|
|
875
|
+
console.debug(` Header logo file: ${resolvedHeaderLogoPath}`);
|
|
876
|
+
}
|
|
877
|
+
if (serverConfig.rateLimit && serverConfig.rateLimit.enabled !== false) {
|
|
878
|
+
console.debug(` Guest Rate Limiting: enabled (${serverConfig.rateLimit.maxRequests || 30} req/${(serverConfig.rateLimit.windowMs || 60000) / 1000}s, chat: ${serverConfig.rateLimit.chat?.maxRequests || 10} req/${(serverConfig.rateLimit.chat?.windowMs || 60000) / 1000}s)`);
|
|
879
|
+
}
|
|
791
880
|
const startupAdapters = loadAdaptersConfig();
|
|
792
881
|
if (startupAdapters) {
|
|
793
882
|
console.debug(` Available Adapters: ${Object.keys(startupAdapters).join(', ')}`);
|
|
@@ -795,21 +884,19 @@ function main() {
|
|
|
795
884
|
console.debug(` Warning: No adapters configured. Set ORBIT_ADAPTERS or VITE_ADAPTERS environment variable.`);
|
|
796
885
|
}
|
|
797
886
|
console.debug('');
|
|
798
|
-
|
|
887
|
+
|
|
799
888
|
if (serverConfig.open) {
|
|
800
889
|
openBrowser(url);
|
|
801
890
|
}
|
|
802
891
|
});
|
|
803
892
|
|
|
804
|
-
// Handle graceful shutdown
|
|
805
893
|
process.on('SIGINT', () => {
|
|
806
894
|
console.debug('\n\nShutting down server...');
|
|
807
895
|
process.exit(0);
|
|
808
896
|
});
|
|
809
897
|
}
|
|
810
898
|
|
|
811
|
-
// Run if called directly
|
|
812
|
-
// For ES modules, we check if this file is being executed directly
|
|
899
|
+
// Run if called directly
|
|
813
900
|
const isMainModule = process.argv[1] && (
|
|
814
901
|
import.meta.url === `file://${process.argv[1]}` ||
|
|
815
902
|
import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/')) ||
|
|
@@ -821,4 +908,4 @@ if (isMainModule) {
|
|
|
821
908
|
main();
|
|
822
909
|
}
|
|
823
910
|
|
|
824
|
-
export { main,
|
|
911
|
+
export { main, createServer, loadAdaptersConfig };
|