@r8s/librechat 0.2.0 → 0.3.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/dist/index.d.ts +144 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +219 -0
- package/dist/index.js.map +1 -0
- package/package.json +14 -7
- package/__tests__/librechat.test.ts +0 -545
- package/examples/basic.tsx +0 -17
- package/src/index.tsx +0 -384
- package/tsconfig.json +0 -15
|
@@ -1,545 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { render, jsx } from '@r8s/core'
|
|
3
|
-
import { OperatorContext, SecretContext, RoutingContext } from '@r8s/core/defaults'
|
|
4
|
-
import { runGuardrails, noPlaintextSecrets, validateResource } from '@r8s/core'
|
|
5
|
-
import { operators } from '@r8s/crds'
|
|
6
|
-
import type { r8sElement } from '@r8s/core'
|
|
7
|
-
|
|
8
|
-
// LibreChat recipe tests:
|
|
9
|
-
// 1. Operator declarations (deduped via OperatorContext)
|
|
10
|
-
// 2. Rendering: defaults, Meilisearch, all props, gateway/ingress adaptation
|
|
11
|
-
// 3. MongoDB wiring: MONGO_URI expansion + secretKeyRef credentials
|
|
12
|
-
// 4. Env wiring: reverse proxy, redis, search, session credentials,
|
|
13
|
-
// upstream env names, no duplicate env names
|
|
14
|
-
// 5. Security: no plaintext credentials in rendered output
|
|
15
|
-
import { LibreChat } from '../src/index'
|
|
16
|
-
|
|
17
|
-
const openbao = { backend: 'openbao', mount: 'kv', path: 'test' }
|
|
18
|
-
|
|
19
|
-
const mongodb = {
|
|
20
|
-
host: 'mongo.data.svc.cluster.local',
|
|
21
|
-
port: 27017,
|
|
22
|
-
passwordSecret: 'chat-mongodb-credentials',
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const sso = {
|
|
26
|
-
issuer: 'https://keycloak.example.com/realms/platform',
|
|
27
|
-
clientId: 'librechat',
|
|
28
|
-
clientSecretRef: { secret: 'librechat-sso', key: 'clientSecret' },
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Render LibreChat inside a Platform-like secrets backend (OpenBao). */
|
|
32
|
-
function renderLibreChat(props: Record<string, unknown>): ReturnType<typeof render> {
|
|
33
|
-
return render(
|
|
34
|
-
jsx(SecretContext.Provider, {
|
|
35
|
-
value: openbao as never,
|
|
36
|
-
children: jsx(LibreChat, props as never),
|
|
37
|
-
})
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Wrap LibreChat in an OperatorContext (no secrets backend). */
|
|
42
|
-
function elementWithContext(ops: any[], props: Record<string, unknown>): r8sElement {
|
|
43
|
-
return jsx(OperatorContext.Provider, {
|
|
44
|
-
value: ops,
|
|
45
|
-
children: jsx(LibreChat, props as never),
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* k8s rejects duplicate env names in one container — assert every env
|
|
51
|
-
* list we render is unique (this bit us with `$(VAR)` self-echoes of
|
|
52
|
-
* secretKeyRef-backed vars).
|
|
53
|
-
*/
|
|
54
|
-
function assertUniqueEnvNames(env: Array<{ name: string }>): void {
|
|
55
|
-
const names = env.map((e) => e.name)
|
|
56
|
-
const dupes = names.filter((n, i) => names.indexOf(n) !== i)
|
|
57
|
-
expect(dupes).toEqual([])
|
|
58
|
-
expect(new Set(names).size).toBe(names.length)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
describe('operator declarations', () => {
|
|
62
|
-
it('declares the redis operator when cache is enabled', () => {
|
|
63
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb })
|
|
64
|
-
expect(result.operators.some((op) => op.name === 'redis-operator')).toBe(true)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('skips the redis operator when cache is disabled', () => {
|
|
68
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb, cache: false })
|
|
69
|
-
expect(result.operators.some((op) => op.name === 'redis-operator')).toBe(false)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('never declares cnpg (MongoDB is provisioned externally)', () => {
|
|
73
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb })
|
|
74
|
-
expect(result.operators.some((op) => op.name === 'cnpg')).toBe(false)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('deduplicates operators provided via context', () => {
|
|
78
|
-
const result = render(
|
|
79
|
-
elementWithContext([operators['redis-operator']()], {
|
|
80
|
-
host: 'chat.example.com',
|
|
81
|
-
mongodb,
|
|
82
|
-
secretsName: 'existing-secrets',
|
|
83
|
-
})
|
|
84
|
-
)
|
|
85
|
-
const names = result.operators.map((op) => op.name)
|
|
86
|
-
expect(names.filter((n) => n === 'redis-operator')).toHaveLength(1)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('allows version overrides through context operators', () => {
|
|
90
|
-
const result = render(
|
|
91
|
-
elementWithContext([operators['redis-operator']('1.0.0')], {
|
|
92
|
-
host: 'chat.example.com',
|
|
93
|
-
mongodb,
|
|
94
|
-
secretsName: 'existing-secrets',
|
|
95
|
-
})
|
|
96
|
-
)
|
|
97
|
-
const redis = result.operators.filter((op) => op.name === 'redis-operator')
|
|
98
|
-
expect(redis).toHaveLength(1)
|
|
99
|
-
expect(redis[0].version).toBe('1.0.0')
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
describe('rendering defaults', () => {
|
|
104
|
-
it('renders app deployment, service, ingress and redis', () => {
|
|
105
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb })
|
|
106
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
107
|
-
expect(kinds).toContain('Deployment')
|
|
108
|
-
expect(kinds).toContain('Service')
|
|
109
|
-
expect(kinds).toContain('Ingress')
|
|
110
|
-
expect(kinds).toContain('RedisReplication')
|
|
111
|
-
expect(kinds).not.toContain('RedisCluster')
|
|
112
|
-
expect(kinds).toContain('OpenBaoStaticSecret')
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('renders a Meilisearch deployment with health probes and resources when search is enabled', () => {
|
|
116
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb, search: true })
|
|
117
|
-
const deployments = result.resources.filter((r) => r.kind === 'Deployment')
|
|
118
|
-
const services = result.resources.filter((r) => r.kind === 'Service')
|
|
119
|
-
expect(deployments.map((d: any) => d.metadata.name)).toContain('librechat-meilisearch')
|
|
120
|
-
expect(services.map((s: any) => s.metadata.name)).toContain('librechat-meilisearch')
|
|
121
|
-
const meili = deployments.find((d: any) => d.metadata.name === 'librechat-meilisearch') as any
|
|
122
|
-
expect(meili.spec.template.spec.containers[0].image).toBe('getmeili/meilisearch:v1.6')
|
|
123
|
-
const meiliContainer = meili.spec.template.spec.containers[0]
|
|
124
|
-
expect(meiliContainer.livenessProbe.httpGet).toEqual({ path: '/health', port: 7700 })
|
|
125
|
-
expect(meiliContainer.readinessProbe.httpGet).toEqual({ path: '/health', port: 7700 })
|
|
126
|
-
expect(meiliContainer.resources).toEqual({
|
|
127
|
-
requests: { memory: '256Mi', cpu: '100m' },
|
|
128
|
-
limits: { memory: '1Gi', cpu: '500m' },
|
|
129
|
-
})
|
|
130
|
-
assertUniqueEnvNames(meiliContainer.env)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
it('renders no Meilisearch resources and no SEARCH flag when search is disabled', () => {
|
|
134
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb })
|
|
135
|
-
const names = result.resources
|
|
136
|
-
.filter((r) => r.kind === 'Deployment' || r.kind === 'Service')
|
|
137
|
-
.map((r: any) => r.metadata.name)
|
|
138
|
-
expect(names).not.toContain('librechat-meilisearch')
|
|
139
|
-
const app = result.resources.find(
|
|
140
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
141
|
-
) as any
|
|
142
|
-
const env = app.spec.template.spec.containers[0].env
|
|
143
|
-
expect(env.find((e: any) => e.name === 'SEARCH')).toBeUndefined()
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('quotes the redis master service and enables USE_REDIS', () => {
|
|
147
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb })
|
|
148
|
-
const redis = result.resources.find((r) => r.kind === 'RedisReplication') as any
|
|
149
|
-
expect(redis.metadata.name).toBe('librechat-redis')
|
|
150
|
-
expect(redis.spec.clusterSize).toBe(3)
|
|
151
|
-
expect(redis.spec.kubernetesConfig.image).toBe('redis:7.2-alpine')
|
|
152
|
-
|
|
153
|
-
const app = result.resources.find(
|
|
154
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
155
|
-
) as any
|
|
156
|
-
const env = app.spec.template.spec.containers[0].env
|
|
157
|
-
expect(env.find((e: any) => e.name === 'USE_REDIS').value).toBe('true')
|
|
158
|
-
expect(env.find((e: any) => e.name === 'REDIS_URI').value).toBe('redis://librechat-redis:6379')
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('renders gateway resources when platform uses gateway routing', () => {
|
|
162
|
-
const result = render(
|
|
163
|
-
jsx(RoutingContext.Provider, {
|
|
164
|
-
value: { mode: 'gateway', gatewayClassName: 'eg' },
|
|
165
|
-
children: jsx(SecretContext.Provider, {
|
|
166
|
-
value: openbao as never,
|
|
167
|
-
children: jsx(LibreChat, { host: 'chat.example.com', mongodb }),
|
|
168
|
-
}),
|
|
169
|
-
})
|
|
170
|
-
)
|
|
171
|
-
const kinds = result.resources.map((r) => r.kind)
|
|
172
|
-
expect(kinds).toContain('HTTPRoute')
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('renders a valid Ingress when platform uses ingress routing', () => {
|
|
176
|
-
const result = render(
|
|
177
|
-
jsx(RoutingContext.Provider, {
|
|
178
|
-
value: { mode: 'ingress' },
|
|
179
|
-
children: jsx(SecretContext.Provider, {
|
|
180
|
-
value: openbao as never,
|
|
181
|
-
children: jsx(LibreChat, { host: 'chat.example.com', mongodb }),
|
|
182
|
-
}),
|
|
183
|
-
})
|
|
184
|
-
)
|
|
185
|
-
const ingress = result.resources.find((r) => r.kind === 'Ingress') as any
|
|
186
|
-
expect(ingress).toBeDefined()
|
|
187
|
-
expect(ingress.spec.rules[0].host).toBe('chat.example.com')
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('sets websocket-friendly proxy annotations on the Ingress', () => {
|
|
191
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb })
|
|
192
|
-
const ingress = result.resources.find((r) => r.kind === 'Ingress') as any
|
|
193
|
-
expect(ingress.metadata.annotations).toMatchObject({
|
|
194
|
-
'nginx.ingress.kubernetes.io/proxy-read-timeout': '300',
|
|
195
|
-
'nginx.ingress.kubernetes.io/proxy-send-timeout': '300',
|
|
196
|
-
'nginx.ingress.kubernetes.io/proxy-buffering': 'off',
|
|
197
|
-
})
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it('passes resource validation', () => {
|
|
201
|
-
const result = renderLibreChat({
|
|
202
|
-
host: 'chat.example.com',
|
|
203
|
-
mongodb,
|
|
204
|
-
search: true,
|
|
205
|
-
sso,
|
|
206
|
-
})
|
|
207
|
-
for (const resource of result.resources) {
|
|
208
|
-
expect(validateResource(resource)).toEqual([])
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
describe('rendering with all props', () => {
|
|
214
|
-
it('accepts the full prop surface', () => {
|
|
215
|
-
const result = renderLibreChat({
|
|
216
|
-
name: 'chat',
|
|
217
|
-
namespace: 'chat',
|
|
218
|
-
version: '0.7.8',
|
|
219
|
-
host: 'chat.example.com',
|
|
220
|
-
port: 3080,
|
|
221
|
-
replicas: 3,
|
|
222
|
-
mongodb: { ...mongodb, username: 'chat' },
|
|
223
|
-
cache: true,
|
|
224
|
-
search: true,
|
|
225
|
-
sso,
|
|
226
|
-
backend: 'https://api.berget.ai/v1',
|
|
227
|
-
resources: {
|
|
228
|
-
requests: { memory: '1Gi', cpu: '500m' },
|
|
229
|
-
limits: { memory: '4Gi', cpu: '2000m' },
|
|
230
|
-
},
|
|
231
|
-
tls: { secretName: 'chat-tls', clusterIssuer: 'letsencrypt-prod' },
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
const app = result.resources.find(
|
|
235
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'chat'
|
|
236
|
-
) as any
|
|
237
|
-
expect(app).toBeDefined()
|
|
238
|
-
expect(app.spec.replicas).toBe(3)
|
|
239
|
-
expect(app.spec.template.spec.containers[0].image).toBe('ghcr.io/danny-avila/librechat:0.7.8')
|
|
240
|
-
expect(app.spec.template.spec.containers[0].resources.limits.memory).toBe('4Gi')
|
|
241
|
-
expect(app.spec.template.spec.containers[0].ports[0].containerPort).toBe(3080)
|
|
242
|
-
assertUniqueEnvNames(app.spec.template.spec.containers[0].env)
|
|
243
|
-
})
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
describe('mongodb wiring', () => {
|
|
247
|
-
it('builds MONGO_URI with $(VAR) expansion and the database named after the resource', () => {
|
|
248
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb })
|
|
249
|
-
const app = result.resources.find(
|
|
250
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
251
|
-
) as any
|
|
252
|
-
const env = app.spec.template.spec.containers[0].env
|
|
253
|
-
const mongoUri = env.find((e: any) => e.name === 'MONGO_URI')
|
|
254
|
-
expect(mongoUri.value).toBe(
|
|
255
|
-
'mongodb://$(MONGO_USERNAME):$(MONGO_PASSWORD)@mongo.data.svc.cluster.local:27017/librechat'
|
|
256
|
-
)
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
it('appends authSource when the mongodb connection declares one', () => {
|
|
260
|
-
const result = renderLibreChat({
|
|
261
|
-
host: 'chat.example.com',
|
|
262
|
-
mongodb: { ...mongodb, authSource: 'admin' },
|
|
263
|
-
})
|
|
264
|
-
const app = result.resources.find(
|
|
265
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
266
|
-
) as any
|
|
267
|
-
const env = app.spec.template.spec.containers[0].env
|
|
268
|
-
expect(env.find((e: any) => e.name === 'MONGO_URI').value).toBe(
|
|
269
|
-
'mongodb://$(MONGO_USERNAME):$(MONGO_PASSWORD)@mongo.data.svc.cluster.local:27017/librechat?authSource=admin'
|
|
270
|
-
)
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
it('delivers MONGO_USERNAME / MONGO_PASSWORD via secretKeyRef from the password secret', () => {
|
|
274
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb })
|
|
275
|
-
const app = result.resources.find(
|
|
276
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
277
|
-
) as any
|
|
278
|
-
const env = app.spec.template.spec.containers[0].env
|
|
279
|
-
const username = env.find((e: any) => e.name === 'MONGO_USERNAME')
|
|
280
|
-
const password = env.find((e: any) => e.name === 'MONGO_PASSWORD')
|
|
281
|
-
expect(username.valueFrom.secretKeyRef).toEqual({
|
|
282
|
-
name: 'chat-mongodb-credentials',
|
|
283
|
-
key: 'username',
|
|
284
|
-
})
|
|
285
|
-
expect(password.valueFrom.secretKeyRef).toEqual({
|
|
286
|
-
name: 'chat-mongodb-credentials',
|
|
287
|
-
key: 'password',
|
|
288
|
-
})
|
|
289
|
-
expect(username.value).toBeUndefined()
|
|
290
|
-
expect(password.value).toBeUndefined()
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
it('inlines an explicitly provided username (identifier, not secret)', () => {
|
|
294
|
-
const result = renderLibreChat({
|
|
295
|
-
host: 'chat.example.com',
|
|
296
|
-
mongodb: { ...mongodb, username: 'chat' },
|
|
297
|
-
})
|
|
298
|
-
const app = result.resources.find(
|
|
299
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
300
|
-
) as any
|
|
301
|
-
const env = app.spec.template.spec.containers[0].env
|
|
302
|
-
const username = env.find((e: any) => e.name === 'MONGO_USERNAME')
|
|
303
|
-
const password = env.find((e: any) => e.name === 'MONGO_PASSWORD')
|
|
304
|
-
expect(username.value).toBe('chat')
|
|
305
|
-
expect(username.valueFrom).toBeUndefined()
|
|
306
|
-
expect(password.valueFrom.secretKeyRef.key).toBe('password')
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
it('shares MEILI_MASTER_KEY with the main app via the secrets bundle when search is on', () => {
|
|
310
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb, search: true })
|
|
311
|
-
const app = result.resources.find(
|
|
312
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
313
|
-
) as any
|
|
314
|
-
const env = app.spec.template.spec.containers[0].env
|
|
315
|
-
expect(env.find((e: any) => e.name === 'SEARCH').value).toBe('true')
|
|
316
|
-
expect(env.find((e: any) => e.name === 'MEILI_HOST').value).toBe(
|
|
317
|
-
'http://librechat-meilisearch:7700'
|
|
318
|
-
)
|
|
319
|
-
const meiliKey = env.find((e: any) => e.name === 'MEILI_MASTER_KEY')
|
|
320
|
-
expect(meiliKey.valueFrom.secretKeyRef.name).toBe('librechat-secrets')
|
|
321
|
-
expect(meiliKey.valueFrom.secretKeyRef.key).toBe('meiliMasterKey')
|
|
322
|
-
expect(meiliKey.value).toBeUndefined()
|
|
323
|
-
assertUniqueEnvNames(env)
|
|
324
|
-
|
|
325
|
-
const meiliDeployment = result.resources.find(
|
|
326
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat-meilisearch'
|
|
327
|
-
) as any
|
|
328
|
-
const meiliEnv = meiliDeployment.spec.template.spec.containers[0].env
|
|
329
|
-
const meiliMasterKey = meiliEnv.find((e: any) => e.name === 'MEILI_MASTER_KEY')
|
|
330
|
-
expect(meiliMasterKey.valueFrom.secretKeyRef.name).toBe('librechat-secrets')
|
|
331
|
-
expect(meiliMasterKey.value).toBeUndefined()
|
|
332
|
-
})
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
describe('session credentials', () => {
|
|
336
|
-
it('wires JWT + CREDS multi-user credentials via secretKeyRef from the secrets bundle', () => {
|
|
337
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb })
|
|
338
|
-
const app = result.resources.find(
|
|
339
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
340
|
-
) as any
|
|
341
|
-
const env = app.spec.template.spec.containers[0].env
|
|
342
|
-
const jwtSecret = env.find((e: any) => e.name === 'JWT_SECRET')
|
|
343
|
-
const jwtRefresh = env.find((e: any) => e.name === 'JWT_REFRESH_SECRET')
|
|
344
|
-
const credsKey = env.find((e: any) => e.name === 'CREDS_KEY')
|
|
345
|
-
const credsIv = env.find((e: any) => e.name === 'CREDS_IV')
|
|
346
|
-
expect(jwtSecret).toMatchObject({
|
|
347
|
-
valueFrom: { secretKeyRef: { name: 'librechat-secrets', key: 'jwtSecret' } },
|
|
348
|
-
})
|
|
349
|
-
expect(jwtRefresh).toMatchObject({
|
|
350
|
-
valueFrom: { secretKeyRef: { name: 'librechat-secrets', key: 'jwtRefreshSecret' } },
|
|
351
|
-
})
|
|
352
|
-
expect(credsKey).toMatchObject({
|
|
353
|
-
valueFrom: { secretKeyRef: { name: 'librechat-secrets', key: 'credsKey' } },
|
|
354
|
-
})
|
|
355
|
-
expect(credsIv).toMatchObject({
|
|
356
|
-
valueFrom: { secretKeyRef: { name: 'librechat-secrets', key: 'credsIv' } },
|
|
357
|
-
})
|
|
358
|
-
for (const e of [jwtSecret, jwtRefresh, credsKey, credsIv]) {
|
|
359
|
-
expect(e.value).toBeUndefined()
|
|
360
|
-
}
|
|
361
|
-
// REFRESH_TOKEN_EXPIRY is a plain TTL — delivered via configMapKeyRef
|
|
362
|
-
const refreshTokenExpiry = env.find((e: any) => e.name === 'REFRESH_TOKEN_EXPIRY')
|
|
363
|
-
expect(refreshTokenExpiry.valueFrom.configMapKeyRef).toEqual({
|
|
364
|
-
name: 'librechat-config',
|
|
365
|
-
key: 'REFRESH_TOKEN_EXPIRY',
|
|
366
|
-
})
|
|
367
|
-
expect(refreshTokenExpiry.value).toBeUndefined()
|
|
368
|
-
const configMap = result.resources.find(
|
|
369
|
-
(r: any) => r.kind === 'ConfigMap' && r.metadata.name === 'librechat-config'
|
|
370
|
-
) as any
|
|
371
|
-
expect(configMap.data).toEqual({ REFRESH_TOKEN_EXPIRY: '604800' })
|
|
372
|
-
assertUniqueEnvNames(env)
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
it('resolves session credential keys against an explicit secretsName', () => {
|
|
376
|
-
const result = renderLibreChat({
|
|
377
|
-
host: 'chat.example.com',
|
|
378
|
-
mongodb,
|
|
379
|
-
secretsName: 'existing-secrets',
|
|
380
|
-
})
|
|
381
|
-
const app = result.resources.find(
|
|
382
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
383
|
-
) as any
|
|
384
|
-
const env = app.spec.template.spec.containers[0].env
|
|
385
|
-
for (const envName of ['JWT_SECRET', 'JWT_REFRESH_SECRET', 'CREDS_KEY', 'CREDS_IV']) {
|
|
386
|
-
expect(env.find((e: any) => e.name === envName).valueFrom.secretKeyRef.name).toBe(
|
|
387
|
-
'existing-secrets'
|
|
388
|
-
)
|
|
389
|
-
}
|
|
390
|
-
})
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
describe('secrets handling', () => {
|
|
394
|
-
it('provisions the app secrets bundle through a secrets backend', () => {
|
|
395
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb })
|
|
396
|
-
const bao = result.resources.find((r) => r.kind === 'OpenBaoStaticSecret') as any
|
|
397
|
-
expect(bao).toBeDefined()
|
|
398
|
-
expect(bao.spec.destination.name).toBe('librechat-secrets')
|
|
399
|
-
expect(bao.spec.path).toBe('test/librechat/secrets')
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
it('provisions the app secrets bundle through Vault', () => {
|
|
403
|
-
const result = render(
|
|
404
|
-
jsx(SecretContext.Provider, {
|
|
405
|
-
value: { backend: 'vault', mount: 'kv', path: 'apps' },
|
|
406
|
-
children: jsx(LibreChat, { host: 'chat.example.com', mongodb }),
|
|
407
|
-
})
|
|
408
|
-
)
|
|
409
|
-
const vault = result.resources.find((r) => r.kind === 'VaultStaticSecret') as any
|
|
410
|
-
expect(vault).toBeDefined()
|
|
411
|
-
expect(vault.spec.destination.name).toBe('librechat-secrets')
|
|
412
|
-
expect(vault.spec.path).toBe('apps/librechat/secrets')
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
it('accepts an existing secretsName without a backend', () => {
|
|
416
|
-
expect(() =>
|
|
417
|
-
render(
|
|
418
|
-
jsx(LibreChat, {
|
|
419
|
-
host: 'chat.example.com',
|
|
420
|
-
mongodb,
|
|
421
|
-
secretsName: 'existing-secrets',
|
|
422
|
-
})
|
|
423
|
-
)
|
|
424
|
-
).not.toThrow()
|
|
425
|
-
const result = render(
|
|
426
|
-
jsx(LibreChat, {
|
|
427
|
-
host: 'chat.example.com',
|
|
428
|
-
mongodb,
|
|
429
|
-
secretsName: 'existing-secrets',
|
|
430
|
-
})
|
|
431
|
-
)
|
|
432
|
-
expect(
|
|
433
|
-
result.resources.find(
|
|
434
|
-
(r) => r.kind === 'OpenBaoStaticSecret' || r.kind === 'VaultStaticSecret'
|
|
435
|
-
)
|
|
436
|
-
).toBeUndefined()
|
|
437
|
-
const app = result.resources.find(
|
|
438
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
439
|
-
) as any
|
|
440
|
-
const env = app.spec.template.spec.containers[0].env
|
|
441
|
-
expect(env.find((e: any) => e.name === 'SECRET_KEY').valueFrom.secretKeyRef.name).toBe(
|
|
442
|
-
'existing-secrets'
|
|
443
|
-
)
|
|
444
|
-
})
|
|
445
|
-
|
|
446
|
-
it('throws when no secrets backend and no secretsName', () => {
|
|
447
|
-
expect(() => render(jsx(LibreChat, { host: 'chat.example.com', mongodb }))).toThrow(
|
|
448
|
-
/application secrets/
|
|
449
|
-
)
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
it('wires app credentials via secretKeyRef (never plaintext env)', () => {
|
|
453
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb })
|
|
454
|
-
const app = result.resources.find(
|
|
455
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
456
|
-
) as any
|
|
457
|
-
const env = app.spec.template.spec.containers[0].env
|
|
458
|
-
const secretKey = env.find((e: any) => e.name === 'SECRET_KEY')
|
|
459
|
-
const modelApiKey = env.find((e: any) => e.name === 'OPENAI_API_KEY')
|
|
460
|
-
expect(secretKey.valueFrom.secretKeyRef.name).toBe('librechat-secrets')
|
|
461
|
-
expect(secretKey.valueFrom.secretKeyRef.key).toBe('secretKey')
|
|
462
|
-
expect(modelApiKey.valueFrom.secretKeyRef.name).toBe('librechat-secrets')
|
|
463
|
-
expect(modelApiKey.valueFrom.secretKeyRef.key).toBe('modelApiKey')
|
|
464
|
-
expect(secretKey.value).toBeUndefined()
|
|
465
|
-
expect(modelApiKey.value).toBeUndefined()
|
|
466
|
-
})
|
|
467
|
-
|
|
468
|
-
it('renders no plaintext credentials anywhere', () => {
|
|
469
|
-
const result = renderLibreChat({
|
|
470
|
-
host: 'chat.example.com',
|
|
471
|
-
mongodb,
|
|
472
|
-
search: true,
|
|
473
|
-
sso,
|
|
474
|
-
})
|
|
475
|
-
const { passed, errors } = runGuardrails(result.resources as any[], [noPlaintextSecrets])
|
|
476
|
-
if (!passed) {
|
|
477
|
-
console.error('Plaintext credential violations:', errors)
|
|
478
|
-
}
|
|
479
|
-
expect(passed).toBe(true)
|
|
480
|
-
})
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
describe('sso wiring', () => {
|
|
484
|
-
it('sets OPENID env vars and wires the client secret via secretKeyRef', () => {
|
|
485
|
-
const result = renderLibreChat({ host: 'chat.example.com', mongodb, sso })
|
|
486
|
-
const app = result.resources.find(
|
|
487
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
488
|
-
) as any
|
|
489
|
-
const env = app.spec.template.spec.containers[0].env
|
|
490
|
-
const names = env.map((e: any) => e.name)
|
|
491
|
-
assertUniqueEnvNames(env)
|
|
492
|
-
expect(env.find((e: any) => e.name === 'ALLOW_SOCIAL_LOGIN').value).toBe('true')
|
|
493
|
-
expect(env.find((e: any) => e.name === 'DOMAIN_SERVER').value).toBe('https://chat.example.com')
|
|
494
|
-
expect(env.find((e: any) => e.name === 'DOMAIN_CLIENT').value).toBe('https://chat.example.com')
|
|
495
|
-
expect(env.find((e: any) => e.name === 'OPENID_ISSUER').value).toBe(sso.issuer)
|
|
496
|
-
expect(env.find((e: any) => e.name === 'OPENID_CLIENT_ID').value).toBe(sso.clientId)
|
|
497
|
-
expect(env.find((e: any) => e.name === 'OPENID_SCOPES').value).toBe('openid profile email')
|
|
498
|
-
expect(env.find((e: any) => e.name === 'OPENID_CALLBACK_URL').value).toBe(
|
|
499
|
-
'https://chat.example.com/oauth/openid/callback'
|
|
500
|
-
)
|
|
501
|
-
const clientSecret = env.find((e: any) => e.name === 'OPENID_CLIENT_SECRET')
|
|
502
|
-
expect(clientSecret.valueFrom.secretKeyRef).toEqual({
|
|
503
|
-
name: 'librechat-sso',
|
|
504
|
-
key: 'clientSecret',
|
|
505
|
-
})
|
|
506
|
-
expect(clientSecret.value).toBeUndefined()
|
|
507
|
-
// no $(VAR) self-echo of the secretKeyRef-backed var
|
|
508
|
-
expect(names).not.toContain('OPENID_SCOPE')
|
|
509
|
-
})
|
|
510
|
-
})
|
|
511
|
-
|
|
512
|
-
describe('openai backend wiring', () => {
|
|
513
|
-
it('routes model calls through OPENAI_REVERSE_PROXY (no OPENAI_API_BASE_URL)', () => {
|
|
514
|
-
const result = renderLibreChat({
|
|
515
|
-
host: 'chat.example.com',
|
|
516
|
-
mongodb,
|
|
517
|
-
secretsName: 'existing-secrets',
|
|
518
|
-
})
|
|
519
|
-
const app = result.resources.find(
|
|
520
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
521
|
-
) as any
|
|
522
|
-
const env = app.spec.template.spec.containers[0].env
|
|
523
|
-
const names = env.map((e: any) => e.name)
|
|
524
|
-
expect(env.find((e: any) => e.name === 'OPENAI_REVERSE_PROXY').value).toBe(
|
|
525
|
-
'https://api.berget.ai/v1/chat/completions'
|
|
526
|
-
)
|
|
527
|
-
expect(names).not.toContain('OPENAI_API_BASE_URL')
|
|
528
|
-
})
|
|
529
|
-
|
|
530
|
-
it('honors a backend override with the chat/completions suffix', () => {
|
|
531
|
-
const result = renderLibreChat({
|
|
532
|
-
host: 'chat.example.com',
|
|
533
|
-
mongodb,
|
|
534
|
-
backend: 'https://api.example.com/v1',
|
|
535
|
-
secretsName: 'existing-secrets',
|
|
536
|
-
})
|
|
537
|
-
const app = result.resources.find(
|
|
538
|
-
(r: any) => r.kind === 'Deployment' && r.metadata.name === 'librechat'
|
|
539
|
-
) as any
|
|
540
|
-
const env = app.spec.template.spec.containers[0].env
|
|
541
|
-
expect(env.find((e: any) => e.name === 'OPENAI_REVERSE_PROXY').value).toBe(
|
|
542
|
-
'https://api.example.com/v1/chat/completions'
|
|
543
|
-
)
|
|
544
|
-
})
|
|
545
|
-
})
|
package/examples/basic.tsx
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { Platform } from '@r8s/recipes'
|
|
2
|
-
import { LibreChat } from '@r8s/librechat'
|
|
3
|
-
|
|
4
|
-
export default (
|
|
5
|
-
<Platform secrets={{ backend: 'openbao', mount: 'kv', path: 'apps' }}>
|
|
6
|
-
<LibreChat
|
|
7
|
-
name="chat"
|
|
8
|
-
host="chat.example.com"
|
|
9
|
-
mongodb={{ host: 'mongo.data.svc.cluster.local', passwordSecret: 'chat-mongodb-credentials' }}
|
|
10
|
-
sso={{
|
|
11
|
-
issuer: 'https://keycloak.example.com/realms/platform',
|
|
12
|
-
clientId: 'librechat',
|
|
13
|
-
clientSecretRef: { secret: 'librechat-sso', key: 'clientSecret' },
|
|
14
|
-
}}
|
|
15
|
-
/>
|
|
16
|
-
</Platform>
|
|
17
|
-
)
|