@lobehub/chat 1.79.6 → 1.79.8
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/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/docs/development/database-schema.dbml +119 -0
- package/docs/self-hosting/advanced/online-search.mdx +63 -0
- package/locales/ar/models.json +12 -0
- package/locales/ar/oauth.json +39 -0
- package/locales/bg-BG/models.json +12 -0
- package/locales/bg-BG/oauth.json +39 -0
- package/locales/de-DE/models.json +12 -0
- package/locales/de-DE/oauth.json +39 -0
- package/locales/en-US/models.json +12 -0
- package/locales/en-US/oauth.json +39 -0
- package/locales/es-ES/models.json +12 -0
- package/locales/es-ES/oauth.json +39 -0
- package/locales/fa-IR/models.json +12 -0
- package/locales/fa-IR/oauth.json +39 -0
- package/locales/fr-FR/models.json +12 -0
- package/locales/fr-FR/oauth.json +39 -0
- package/locales/it-IT/models.json +12 -0
- package/locales/it-IT/oauth.json +39 -0
- package/locales/ja-JP/models.json +12 -0
- package/locales/ja-JP/oauth.json +39 -0
- package/locales/ko-KR/models.json +12 -0
- package/locales/ko-KR/oauth.json +39 -0
- package/locales/nl-NL/models.json +12 -0
- package/locales/nl-NL/oauth.json +39 -0
- package/locales/pl-PL/models.json +12 -0
- package/locales/pl-PL/oauth.json +39 -0
- package/locales/pt-BR/models.json +12 -0
- package/locales/pt-BR/oauth.json +39 -0
- package/locales/ru-RU/models.json +12 -0
- package/locales/ru-RU/oauth.json +39 -0
- package/locales/tr-TR/models.json +12 -0
- package/locales/tr-TR/oauth.json +39 -0
- package/locales/vi-VN/models.json +12 -0
- package/locales/vi-VN/oauth.json +39 -0
- package/locales/zh-CN/models.json +12 -0
- package/locales/zh-CN/oauth.json +39 -0
- package/locales/zh-TW/models.json +12 -0
- package/locales/zh-TW/oauth.json +39 -0
- package/package.json +5 -2
- package/scripts/generate-oidc-jwk.mjs +59 -0
- package/scripts/migrateServerDB/index.ts +3 -1
- package/src/app/(backend)/oidc/[...oidc]/route.ts +270 -0
- package/src/app/(backend)/oidc/consent/route.ts +97 -0
- package/src/app/[variants]/oauth/consent/[uid]/Client.tsx +97 -0
- package/src/app/[variants]/oauth/consent/[uid]/failed/page.tsx +36 -0
- package/src/app/[variants]/oauth/consent/[uid]/page.tsx +71 -0
- package/src/app/[variants]/oauth/consent/[uid]/success/page.tsx +30 -0
- package/src/const/hotkeys.ts +2 -2
- package/src/const/trace.ts +1 -0
- package/src/database/client/migrations.json +27 -8
- package/src/database/migrations/0020_add_oidc.sql +124 -0
- package/src/database/migrations/meta/0020_snapshot.json +4975 -0
- package/src/database/migrations/meta/_journal.json +7 -0
- package/src/database/repositories/tableViewer/index.test.ts +1 -1
- package/src/database/schemas/index.ts +1 -0
- package/src/database/schemas/oidc.ts +158 -0
- package/src/database/server/models/__tests__/adapter.test.ts +503 -0
- package/src/envs/oidc.ts +18 -0
- package/src/libs/agent-runtime/azureOpenai/index.ts +4 -1
- package/src/libs/agent-runtime/utils/streams/protocol.ts +2 -4
- package/src/libs/oidc-provider/adapter.ts +494 -0
- package/src/libs/oidc-provider/config.ts +53 -0
- package/src/libs/oidc-provider/http-adapter.ts +279 -0
- package/src/libs/oidc-provider/interaction-policy.ts +37 -0
- package/src/libs/oidc-provider/provider.ts +260 -0
- package/src/locales/default/index.ts +2 -0
- package/src/locales/default/oauth.ts +41 -0
- package/src/middleware.ts +94 -6
- package/src/server/services/oidc/index.ts +29 -0
- package/src/server/services/oidc/oidcProvider.ts +27 -0
- package/src/store/chat/slices/aiChat/actions/memory.ts +6 -1
- package/src/types/hotkey.ts +54 -3
package/src/envs/oidc.ts
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
import { createEnv } from '@t3-oss/env-nextjs';
|
2
|
+
import { z } from 'zod';
|
3
|
+
|
4
|
+
export const oidcEnv = createEnv({
|
5
|
+
client: {},
|
6
|
+
runtimeEnv: {
|
7
|
+
ENABLE_OIDC: process.env.ENABLE_OIDC === '1',
|
8
|
+
OIDC_JWKS_KEY: process.env.OIDC_JWKS_KEY,
|
9
|
+
},
|
10
|
+
server: {
|
11
|
+
// 是否启用 OIDC
|
12
|
+
ENABLE_OIDC: z.boolean().optional().default(false),
|
13
|
+
// OIDC 签名密钥
|
14
|
+
// 必须是一个包含私钥的 JWKS (JSON Web Key Set) 格式的 JSON 字符串。
|
15
|
+
// 可以使用 `node scripts/generate-oidc-jwk.mjs` 命令生成。
|
16
|
+
OIDC_JWKS_KEY: z.string().optional(),
|
17
|
+
},
|
18
|
+
});
|
@@ -9,6 +9,7 @@ import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../typ
|
|
9
9
|
import { AgentRuntimeError } from '../utils/createError';
|
10
10
|
import { debugStream } from '../utils/debugStream';
|
11
11
|
import { transformResponseToStream } from '../utils/openaiCompatibleFactory';
|
12
|
+
import { convertOpenAIMessages } from '../utils/openaiHelpers';
|
12
13
|
import { StreamingResponse } from '../utils/response';
|
13
14
|
import { OpenAIStream } from '../utils/streams';
|
14
15
|
|
@@ -49,7 +50,9 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
|
|
49
50
|
|
50
51
|
try {
|
51
52
|
const response = await this.client.chat.completions.create({
|
52
|
-
messages:
|
53
|
+
messages: await convertOpenAIMessages(
|
54
|
+
updatedMessages as OpenAI.ChatCompletionMessageParam[],
|
55
|
+
),
|
53
56
|
model,
|
54
57
|
...params,
|
55
58
|
max_completion_tokens: undefined,
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { ModelTokensUsage } from '@/types/message';
|
2
|
+
import { safeParseJSON } from '@/utils/safeParseJSON';
|
2
3
|
|
3
4
|
import { AgentRuntimeErrorType } from '../../error';
|
4
5
|
import { parseToolCalls } from '../../helpers';
|
@@ -184,10 +185,7 @@ export function createCallbacksTransformer(cb: ChatStreamCallbacks | undefined)
|
|
184
185
|
else if (chunk.startsWith('data:')) {
|
185
186
|
const content = chunk.split('data:')[1].trim();
|
186
187
|
|
187
|
-
|
188
|
-
try {
|
189
|
-
data = JSON.parse(content);
|
190
|
-
} catch {}
|
188
|
+
const data = safeParseJSON(content) as any;
|
191
189
|
|
192
190
|
if (!data) return;
|
193
191
|
|
@@ -0,0 +1,494 @@
|
|
1
|
+
import debug from 'debug';
|
2
|
+
import { eq } from 'drizzle-orm/expressions';
|
3
|
+
|
4
|
+
import {
|
5
|
+
oidcAccessTokens,
|
6
|
+
oidcAuthorizationCodes,
|
7
|
+
oidcClients,
|
8
|
+
oidcDeviceCodes,
|
9
|
+
oidcGrants,
|
10
|
+
oidcInteractions,
|
11
|
+
oidcRefreshTokens,
|
12
|
+
oidcSessions,
|
13
|
+
} from '@/database/schemas/oidc';
|
14
|
+
import { LobeChatDatabase } from '@/database/type';
|
15
|
+
|
16
|
+
// 创建 adapter 日志命名空间
|
17
|
+
const log = debug('lobe-oidc:adapter');
|
18
|
+
|
19
|
+
class OIDCAdapter {
|
20
|
+
private db: LobeChatDatabase;
|
21
|
+
private name: string;
|
22
|
+
|
23
|
+
constructor(name: string, db: LobeChatDatabase) {
|
24
|
+
this.name = name;
|
25
|
+
this.db = db;
|
26
|
+
log('Creating adapter for model: %s', name);
|
27
|
+
}
|
28
|
+
|
29
|
+
/**
|
30
|
+
* 根据模型名称获取对应的数据库表
|
31
|
+
*/
|
32
|
+
private getTable() {
|
33
|
+
log('Getting table for model: %s', this.name);
|
34
|
+
switch (this.name) {
|
35
|
+
case 'AccessToken': {
|
36
|
+
return oidcAccessTokens;
|
37
|
+
}
|
38
|
+
case 'AuthorizationCode': {
|
39
|
+
return oidcAuthorizationCodes;
|
40
|
+
}
|
41
|
+
case 'RefreshToken': {
|
42
|
+
return oidcRefreshTokens;
|
43
|
+
}
|
44
|
+
case 'DeviceCode': {
|
45
|
+
return oidcDeviceCodes;
|
46
|
+
}
|
47
|
+
case 'ClientCredentials': {
|
48
|
+
return oidcAccessTokens;
|
49
|
+
} // 使用相同的表
|
50
|
+
case 'Client': {
|
51
|
+
return oidcClients;
|
52
|
+
}
|
53
|
+
case 'InitialAccessToken': {
|
54
|
+
return oidcAccessTokens;
|
55
|
+
} // 使用相同的表
|
56
|
+
case 'RegistrationAccessToken': {
|
57
|
+
return oidcAccessTokens;
|
58
|
+
} // 使用相同的表
|
59
|
+
case 'Interaction': {
|
60
|
+
return oidcInteractions;
|
61
|
+
}
|
62
|
+
case 'ReplayDetection': {
|
63
|
+
log('ReplayDetection - no persistent storage needed');
|
64
|
+
return null;
|
65
|
+
} // 不需要持久化
|
66
|
+
case 'PushedAuthorizationRequest': {
|
67
|
+
return oidcAuthorizationCodes;
|
68
|
+
} // 使用相同的表
|
69
|
+
case 'Grant': {
|
70
|
+
return oidcGrants;
|
71
|
+
}
|
72
|
+
case 'Session': {
|
73
|
+
return oidcSessions;
|
74
|
+
}
|
75
|
+
default: {
|
76
|
+
const error = `不支持的模型: ${this.name}`;
|
77
|
+
log('ERROR: %s', error);
|
78
|
+
throw new Error(error);
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
/**
|
84
|
+
* 创建模型实例
|
85
|
+
*/
|
86
|
+
async upsert(id: string, payload: any, expiresIn: number): Promise<void> {
|
87
|
+
log('[%s] upsert called - id: %s, expiresIn: %d', this.name, id, `${expiresIn}s`);
|
88
|
+
log('[%s] payload: %O', this.name, payload);
|
89
|
+
|
90
|
+
const table = this.getTable();
|
91
|
+
if (!table) {
|
92
|
+
log('[%s] upsert - No table for model, returning early', this.name);
|
93
|
+
return;
|
94
|
+
}
|
95
|
+
|
96
|
+
if (this.name === 'Client') {
|
97
|
+
// 客户端模型特殊处理,直接使用传入的数据
|
98
|
+
log('[Client] Upserting client record');
|
99
|
+
try {
|
100
|
+
await this.db
|
101
|
+
.insert(table)
|
102
|
+
.values({
|
103
|
+
applicationType: payload.application_type,
|
104
|
+
clientSecret: payload.client_secret,
|
105
|
+
clientUri: payload.client_uri,
|
106
|
+
description: payload.description,
|
107
|
+
grants: payload.grant_types || [],
|
108
|
+
id,
|
109
|
+
isFirstParty: !!payload.isFirstParty,
|
110
|
+
logoUri: payload.logo_uri,
|
111
|
+
name: payload.name,
|
112
|
+
policyUri: payload.policy_uri,
|
113
|
+
redirectUris: payload.redirectUris || [],
|
114
|
+
responseTypes: payload.response_types || [],
|
115
|
+
scopes: Array.isArray(payload.scopes)
|
116
|
+
? payload.scopes
|
117
|
+
: payload.scope
|
118
|
+
? payload.scope.split(' ')
|
119
|
+
: [],
|
120
|
+
tokenEndpointAuthMethod: payload.token_endpoint_auth_method,
|
121
|
+
tosUri: payload.tos_uri,
|
122
|
+
} as any)
|
123
|
+
.onConflictDoUpdate({
|
124
|
+
set: {
|
125
|
+
applicationType: payload.application_type,
|
126
|
+
clientSecret: payload.clientSecret,
|
127
|
+
clientUri: payload.client_uri,
|
128
|
+
description: payload.description,
|
129
|
+
grants: payload.grant_types || [],
|
130
|
+
isFirstParty: !!payload.isFirstParty,
|
131
|
+
logoUri: payload.logo_uri,
|
132
|
+
name: payload.name,
|
133
|
+
policyUri: payload.policy_uri,
|
134
|
+
redirectUris: payload.redirectUris || [],
|
135
|
+
responseTypes: payload.response_types || [],
|
136
|
+
scopes: payload.scope ? payload.scope.split(' ') : [],
|
137
|
+
tokenEndpointAuthMethod: payload.token_endpoint_auth_method,
|
138
|
+
tosUri: payload.tos_uri,
|
139
|
+
} as any,
|
140
|
+
target: (table as any).id,
|
141
|
+
});
|
142
|
+
log('[Client] Successfully upserted client: %s', id);
|
143
|
+
} catch (error) {
|
144
|
+
log('[Client] ERROR upserting client: %O', error);
|
145
|
+
throw error;
|
146
|
+
}
|
147
|
+
return;
|
148
|
+
}
|
149
|
+
|
150
|
+
// 对其他模型,保存完整数据和元数据
|
151
|
+
const expiresAt = expiresIn ? new Date(Date.now() + expiresIn * 1000) : undefined;
|
152
|
+
log('[%s] expiresAt set to: %s', this.name, expiresAt ? expiresAt.toISOString() : 'undefined');
|
153
|
+
|
154
|
+
const record: Record<string, any> = {
|
155
|
+
data: payload,
|
156
|
+
expiresAt,
|
157
|
+
id,
|
158
|
+
};
|
159
|
+
|
160
|
+
// 添加特定字段
|
161
|
+
if (payload.accountId) {
|
162
|
+
record.userId = payload.accountId;
|
163
|
+
log('[%s] Setting userId: %s', this.name, payload.accountId);
|
164
|
+
}
|
165
|
+
|
166
|
+
if (payload.clientId) {
|
167
|
+
record.clientId = payload.clientId;
|
168
|
+
log('[%s] Setting clientId: %s', this.name, payload.clientId);
|
169
|
+
}
|
170
|
+
|
171
|
+
if (payload.grantId) {
|
172
|
+
record.grantId = payload.grantId;
|
173
|
+
log('[%s] Setting grantId: %s', this.name, payload.grantId);
|
174
|
+
}
|
175
|
+
|
176
|
+
if (this.name === 'DeviceCode' && payload.userCode) {
|
177
|
+
record.userCode = payload.userCode;
|
178
|
+
log('[DeviceCode] Setting userCode: %s', payload.userCode);
|
179
|
+
}
|
180
|
+
|
181
|
+
try {
|
182
|
+
log('[%s] Executing upsert DB operation', this.name);
|
183
|
+
await this.db
|
184
|
+
.insert(table)
|
185
|
+
.values(record as any)
|
186
|
+
.onConflictDoUpdate({
|
187
|
+
set: {
|
188
|
+
data: payload,
|
189
|
+
expiresAt,
|
190
|
+
...(payload.accountId ? { userId: payload.accountId } : {}),
|
191
|
+
...(payload.clientId ? { clientId: payload.clientId } : {}),
|
192
|
+
...(payload.grantId ? { grantId: payload.grantId } : {}),
|
193
|
+
...(this.name === 'DeviceCode' && payload.userCode
|
194
|
+
? { userCode: payload.userCode }
|
195
|
+
: {}),
|
196
|
+
} as any,
|
197
|
+
target: (table as any).id,
|
198
|
+
});
|
199
|
+
log('[%s] Successfully upserted record: %s', this.name, id);
|
200
|
+
} catch (error) {
|
201
|
+
log('[%s] ERROR upserting record: %O', this.name, error);
|
202
|
+
console.error(`[OIDC Adapter] Error upserting ${this.name}:`, error);
|
203
|
+
throw error;
|
204
|
+
}
|
205
|
+
}
|
206
|
+
|
207
|
+
/**
|
208
|
+
* 查找模型实例
|
209
|
+
*/
|
210
|
+
async find(id: string): Promise<any> {
|
211
|
+
log('[%s] find called - id: %s', this.name, id);
|
212
|
+
|
213
|
+
const table = this.getTable();
|
214
|
+
if (!table) {
|
215
|
+
log('[%s] find - No table for model, returning undefined', this.name);
|
216
|
+
return undefined;
|
217
|
+
}
|
218
|
+
|
219
|
+
try {
|
220
|
+
log('[%s] Executing find DB query', this.name);
|
221
|
+
const result = await this.db
|
222
|
+
.select()
|
223
|
+
.from(table)
|
224
|
+
.where(eq((table as any).id, id))
|
225
|
+
.limit(1);
|
226
|
+
|
227
|
+
log('[%s] Find query results: %O', this.name, result);
|
228
|
+
|
229
|
+
if (!result || result.length === 0) {
|
230
|
+
log('[%s] No record found for id: %s', this.name, id);
|
231
|
+
return undefined;
|
232
|
+
}
|
233
|
+
|
234
|
+
const model = result[0] as any;
|
235
|
+
|
236
|
+
// 客户端模型特殊处理
|
237
|
+
if (this.name === 'Client') {
|
238
|
+
log('[Client] Converting client record to expected format');
|
239
|
+
return {
|
240
|
+
application_type: model.applicationType,
|
241
|
+
client_id: model.id,
|
242
|
+
client_secret: model.clientSecret,
|
243
|
+
client_uri: model.clientUri,
|
244
|
+
grant_types: model.grants,
|
245
|
+
isFirstParty: model.isFirstParty,
|
246
|
+
logo_uri: model.logoUri,
|
247
|
+
policy_uri: model.policyUri,
|
248
|
+
redirect_uris: model.redirectUris,
|
249
|
+
response_types: model.responseTypes,
|
250
|
+
scope: model.scopes.join(' '),
|
251
|
+
token_endpoint_auth_method: model.tokenEndpointAuthMethod,
|
252
|
+
tos_uri: model.tosUri,
|
253
|
+
};
|
254
|
+
}
|
255
|
+
|
256
|
+
// 如果记录已过期,返回 undefined
|
257
|
+
if (model.expiresAt && new Date() > new Date(model.expiresAt)) {
|
258
|
+
log('[%s] Record expired (expiresAt: %s), returning undefined', this.name, model.expiresAt);
|
259
|
+
return undefined;
|
260
|
+
}
|
261
|
+
|
262
|
+
// 如果记录已被消费,返回 undefined
|
263
|
+
if (model.consumedAt) {
|
264
|
+
log(
|
265
|
+
'[%s] Record already consumed (consumedAt: %s), returning undefined',
|
266
|
+
this.name,
|
267
|
+
model.consumedAt,
|
268
|
+
);
|
269
|
+
return undefined;
|
270
|
+
}
|
271
|
+
|
272
|
+
log('[%s] Successfully found and returning record data', this.name);
|
273
|
+
return model.data;
|
274
|
+
} catch (error) {
|
275
|
+
log('[%s] ERROR finding record: %O', this.name, error);
|
276
|
+
console.error(`[OIDC Adapter] Error finding ${this.name}:`, error);
|
277
|
+
return undefined;
|
278
|
+
}
|
279
|
+
}
|
280
|
+
|
281
|
+
/**
|
282
|
+
* 查找模型实例 by userCode (仅用于设备流程)
|
283
|
+
*/
|
284
|
+
async findByUserCode(userCode: string): Promise<any> {
|
285
|
+
log('[DeviceCode] findByUserCode called - userCode: %s', userCode);
|
286
|
+
|
287
|
+
if (this.name !== 'DeviceCode') {
|
288
|
+
const error = 'findByUserCode 只能用于 DeviceCode 模型';
|
289
|
+
log('ERROR: %s', error);
|
290
|
+
throw new Error(error);
|
291
|
+
}
|
292
|
+
|
293
|
+
try {
|
294
|
+
log('[DeviceCode] Executing findByUserCode DB query');
|
295
|
+
const result = await this.db
|
296
|
+
.select()
|
297
|
+
.from(oidcDeviceCodes)
|
298
|
+
.where(eq(oidcDeviceCodes.userCode, userCode))
|
299
|
+
.limit(1);
|
300
|
+
|
301
|
+
log('[DeviceCode] findByUserCode query results: %O', result);
|
302
|
+
|
303
|
+
if (!result || result.length === 0) {
|
304
|
+
log('[DeviceCode] No record found for userCode: %s', userCode);
|
305
|
+
return undefined;
|
306
|
+
}
|
307
|
+
|
308
|
+
const model = result[0];
|
309
|
+
|
310
|
+
// 如果记录已过期或已被消费,返回 undefined
|
311
|
+
if (model.expiresAt && new Date() > new Date(model.expiresAt)) {
|
312
|
+
log('[DeviceCode] Record expired (expiresAt: %s), returning undefined', model.expiresAt);
|
313
|
+
return undefined;
|
314
|
+
}
|
315
|
+
|
316
|
+
if (model.consumedAt) {
|
317
|
+
log(
|
318
|
+
'[DeviceCode] Record already consumed (consumedAt: %s), returning undefined',
|
319
|
+
model.consumedAt,
|
320
|
+
);
|
321
|
+
return undefined;
|
322
|
+
}
|
323
|
+
|
324
|
+
log('[DeviceCode] Successfully found and returning record data by userCode');
|
325
|
+
return model.data;
|
326
|
+
} catch (error) {
|
327
|
+
log('[DeviceCode] ERROR finding record by userCode: %O', error);
|
328
|
+
console.error('[OIDC Adapter] Error finding DeviceCode by userCode:', error);
|
329
|
+
return undefined;
|
330
|
+
}
|
331
|
+
}
|
332
|
+
|
333
|
+
/**
|
334
|
+
* 查找交互实例 by uid
|
335
|
+
*/
|
336
|
+
async findByUid(uid: string): Promise<any> {
|
337
|
+
log('[Interaction] findByUid called - uid: %s', uid);
|
338
|
+
|
339
|
+
// 复用 find 方法实现
|
340
|
+
log('[Interaction] Delegating to find() method');
|
341
|
+
return this.find(uid);
|
342
|
+
}
|
343
|
+
|
344
|
+
/**
|
345
|
+
* 根据用户 ID 查找会话
|
346
|
+
* 用于会话预同步
|
347
|
+
*/
|
348
|
+
async findSessionByUserId(userId: string): Promise<any> {
|
349
|
+
log('[%s] findSessionByUserId called - userId: %s', this.name, userId);
|
350
|
+
|
351
|
+
if (this.name !== 'Session') {
|
352
|
+
log('[%s] findSessionByUserId - Not a Session model, returning undefined', this.name);
|
353
|
+
return undefined;
|
354
|
+
}
|
355
|
+
|
356
|
+
const table = this.getTable();
|
357
|
+
if (!table) {
|
358
|
+
log('[%s] findSessionByUserId - No table for model, returning undefined', this.name);
|
359
|
+
return undefined;
|
360
|
+
}
|
361
|
+
|
362
|
+
try {
|
363
|
+
log('[%s] Executing findSessionByUserId DB query', this.name);
|
364
|
+
const result = await this.db
|
365
|
+
.select()
|
366
|
+
.from(table)
|
367
|
+
.where(eq((table as any).userId, userId))
|
368
|
+
.limit(1);
|
369
|
+
|
370
|
+
log('[%s] findSessionByUserId query results: %O', this.name, result);
|
371
|
+
|
372
|
+
if (!result || result.length === 0) {
|
373
|
+
log('[%s] No session found for userId: %s', this.name, userId);
|
374
|
+
return undefined;
|
375
|
+
}
|
376
|
+
|
377
|
+
return (result[0] as { data: any }).data;
|
378
|
+
} catch (error) {
|
379
|
+
log('[%s] ERROR finding session by userId: %O', this.name, error);
|
380
|
+
console.error(`[OIDC Adapter] Error finding session by userId:`, error);
|
381
|
+
return undefined;
|
382
|
+
}
|
383
|
+
}
|
384
|
+
|
385
|
+
/**
|
386
|
+
* 销毁模型实例
|
387
|
+
*/
|
388
|
+
async destroy(id: string): Promise<void> {
|
389
|
+
log('[%s] destroy called - id: %s', this.name, id);
|
390
|
+
|
391
|
+
const table = this.getTable();
|
392
|
+
if (!table) {
|
393
|
+
log('[%s] destroy - No table for model, returning early', this.name);
|
394
|
+
return;
|
395
|
+
}
|
396
|
+
|
397
|
+
try {
|
398
|
+
log('[%s] Executing destroy DB operation', this.name);
|
399
|
+
await this.db.delete(table).where(eq((table as any).id, id));
|
400
|
+
log('[%s] Successfully destroyed record: %s', this.name, id);
|
401
|
+
} catch (error) {
|
402
|
+
log('[%s] ERROR destroying record: %O', this.name, error);
|
403
|
+
console.error(`[OIDC Adapter] Error destroying ${this.name}:`, error);
|
404
|
+
throw error;
|
405
|
+
}
|
406
|
+
}
|
407
|
+
|
408
|
+
/**
|
409
|
+
* 标记模型实例为已消费
|
410
|
+
*/
|
411
|
+
async consume(id: string): Promise<void> {
|
412
|
+
log('[%s] consume called - id: %s', this.name, id);
|
413
|
+
|
414
|
+
const table = this.getTable();
|
415
|
+
if (!table) {
|
416
|
+
log('[%s] consume - No table for model, returning early', this.name);
|
417
|
+
return;
|
418
|
+
}
|
419
|
+
|
420
|
+
try {
|
421
|
+
log('[%s] Executing consume DB operation', this.name);
|
422
|
+
await this.db
|
423
|
+
.update(table)
|
424
|
+
// @ts-ignore
|
425
|
+
.set({ consumedAt: new Date() })
|
426
|
+
.where(eq((table as any).id, id));
|
427
|
+
log('[%s] Successfully consumed record: %s', this.name, id);
|
428
|
+
} catch (error) {
|
429
|
+
log('[%s] ERROR consuming record: %O', this.name, error);
|
430
|
+
console.error(`[OIDC Adapter] Error consuming ${this.name}:`, error);
|
431
|
+
throw error;
|
432
|
+
}
|
433
|
+
}
|
434
|
+
|
435
|
+
/**
|
436
|
+
* 根据 grantId 撤销所有相关模型实例
|
437
|
+
*/
|
438
|
+
async revokeByGrantId(grantId: string): Promise<void> {
|
439
|
+
log('[%s] revokeByGrantId called - grantId: %s', this.name, grantId);
|
440
|
+
|
441
|
+
// Grants 本身不需要通过 grantId 来撤销
|
442
|
+
if (this.name === 'Grant') {
|
443
|
+
log('[Grant] revokeByGrantId skipped for Grant model, as it is the grant itself');
|
444
|
+
return;
|
445
|
+
}
|
446
|
+
|
447
|
+
// 提前检查模型名称是否有效,即使后续不直接使用 table
|
448
|
+
this.getTable();
|
449
|
+
|
450
|
+
try {
|
451
|
+
log('[%s] Starting transaction for revokeByGrantId operations', this.name);
|
452
|
+
|
453
|
+
// 使用事务删除所有包含grantId的记录,确保原子性
|
454
|
+
await this.db.transaction(async (tx) => {
|
455
|
+
// 所有可能包含grantId的表
|
456
|
+
const tables = [
|
457
|
+
oidcAccessTokens,
|
458
|
+
oidcAuthorizationCodes,
|
459
|
+
oidcRefreshTokens,
|
460
|
+
oidcDeviceCodes,
|
461
|
+
];
|
462
|
+
|
463
|
+
for (const table of tables) {
|
464
|
+
if ('grantId' in table) {
|
465
|
+
log('[%s] Revoking %s records by grantId: %s', this.name, grantId);
|
466
|
+
await tx.delete(table).where(eq((table as any).grantId, grantId));
|
467
|
+
}
|
468
|
+
}
|
469
|
+
});
|
470
|
+
|
471
|
+
log(
|
472
|
+
'[%s] Successfully completed transaction for revoking all records by grantId: %s',
|
473
|
+
this.name,
|
474
|
+
grantId,
|
475
|
+
);
|
476
|
+
} catch (error) {
|
477
|
+
log('[%s] ERROR in revokeByGrantId transaction: %O', this.name, error);
|
478
|
+
console.error(`[OIDC Adapter] Error in revokeByGrantId transaction:`, error);
|
479
|
+
throw error;
|
480
|
+
}
|
481
|
+
}
|
482
|
+
|
483
|
+
/**
|
484
|
+
* 创建适配器工厂
|
485
|
+
*/
|
486
|
+
static createAdapterFactory(db: LobeChatDatabase) {
|
487
|
+
log('Creating adapter factory with database instance');
|
488
|
+
return function (name: string) {
|
489
|
+
return new OIDCAdapter(name, db);
|
490
|
+
};
|
491
|
+
}
|
492
|
+
}
|
493
|
+
|
494
|
+
export { OIDCAdapter as DrizzleAdapter };
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import { ClientMetadata } from 'oidc-provider';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* 默认 OIDC 客户端配置
|
5
|
+
*/
|
6
|
+
export const defaultClients: ClientMetadata[] = [
|
7
|
+
{
|
8
|
+
// 公共客户端,令牌端点无需认证
|
9
|
+
application_type: 'native',
|
10
|
+
client_id: 'lobehub-desktop',
|
11
|
+
description: 'LobeHub Desktop',
|
12
|
+
// 仅支持授权码流程
|
13
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
14
|
+
// 明确指明是原生应用
|
15
|
+
isFirstParty: true,
|
16
|
+
|
17
|
+
name: 'LobeHub Desktop',
|
18
|
+
|
19
|
+
// 桌面端注册的自定义协议回调(使用反向域名格式)
|
20
|
+
post_logout_redirect_uris: ['com.lobehub.desktop://auth/logout/callback'],
|
21
|
+
|
22
|
+
// 公共客户端,无密钥
|
23
|
+
redirect_uris: ['com.lobehub.desktop://auth/callback', 'https://oauthdebugger.com/debug'],
|
24
|
+
|
25
|
+
// 支持授权码获取令牌和刷新令牌
|
26
|
+
response_types: ['code'],
|
27
|
+
|
28
|
+
// 标记为第一方客户端
|
29
|
+
token_endpoint_auth_method: 'none',
|
30
|
+
},
|
31
|
+
];
|
32
|
+
|
33
|
+
/**
|
34
|
+
* OIDC Scopes 定义
|
35
|
+
*/
|
36
|
+
export const defaultScopes = [
|
37
|
+
'openid', // OIDC 必须
|
38
|
+
'profile', // 请求用户信息(姓名、头像等)
|
39
|
+
'email', // 请求用户邮箱
|
40
|
+
'offline_access', // 请求 Refresh Token
|
41
|
+
'sync:read', // 自定义 Scope:读取同步数据权限
|
42
|
+
'sync:write', // 自定义 Scope:写入同步数据权限
|
43
|
+
];
|
44
|
+
|
45
|
+
/**
|
46
|
+
* OIDC Claims 定义 (与 Scopes 关联)
|
47
|
+
*/
|
48
|
+
export const defaultClaims = {
|
49
|
+
email: ['email', 'email_verified'],
|
50
|
+
openid: ['sub'],
|
51
|
+
// subject (用户唯一标识)
|
52
|
+
profile: ['name', 'picture'],
|
53
|
+
};
|