@lobehub/lobehub 2.0.0-next.354 → 2.0.0-next.356
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/.env.desktop +0 -1
- package/.env.example +16 -20
- package/.env.example.development +1 -4
- package/.github/workflows/e2e.yml +10 -11
- package/CHANGELOG.md +60 -0
- package/Dockerfile +28 -4
- package/changelog/v1.json +18 -0
- package/docker-compose/local/docker-compose.yml +2 -2
- package/docker-compose/local/grafana/docker-compose.yml +2 -2
- package/docker-compose/local/logto/docker-compose.yml +2 -2
- package/docker-compose/local/zitadel/.env.example +2 -2
- package/docker-compose/local/zitadel/.env.zh-CN.example +2 -2
- package/docker-compose/production/grafana/docker-compose.yml +2 -2
- package/docker-compose/production/logto/.env.example +2 -2
- package/docker-compose/production/logto/.env.zh-CN.example +2 -2
- package/docker-compose/production/zitadel/.env.example +2 -2
- package/docker-compose/production/zitadel/.env.zh-CN.example +2 -2
- package/docs/development/basic/add-new-authentication-providers.mdx +144 -136
- package/docs/development/basic/add-new-authentication-providers.zh-CN.mdx +146 -136
- package/docs/self-hosting/advanced/auth/legacy.mdx +4 -0
- package/docs/self-hosting/advanced/auth/legacy.zh-CN.mdx +4 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +326 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +323 -0
- package/docs/self-hosting/advanced/auth.mdx +43 -16
- package/docs/self-hosting/advanced/auth.zh-CN.mdx +44 -16
- package/docs/self-hosting/advanced/redis/upstash.mdx +69 -0
- package/docs/self-hosting/advanced/redis/upstash.zh-CN.mdx +69 -0
- package/docs/self-hosting/advanced/redis.mdx +128 -0
- package/docs/self-hosting/advanced/redis.zh-CN.mdx +126 -0
- package/docs/self-hosting/environment-variables/auth.mdx +15 -1
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +15 -1
- package/docs/self-hosting/environment-variables/basic.mdx +13 -0
- package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +13 -0
- package/docs/self-hosting/environment-variables/redis.mdx +68 -0
- package/docs/self-hosting/environment-variables/redis.zh-CN.mdx +67 -0
- package/docs/self-hosting/migration/v2/breaking-changes.mdx +23 -23
- package/docs/self-hosting/migration/v2/breaking-changes.zh-CN.mdx +23 -23
- package/docs/self-hosting/server-database/docker-compose.mdx +4 -4
- package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +4 -4
- package/e2e/CLAUDE.md +5 -6
- package/e2e/docs/local-setup.md +9 -12
- package/e2e/scripts/setup.ts +9 -15
- package/e2e/src/support/webServer.ts +6 -5
- package/locales/en-US/plugin.json +3 -0
- package/locales/zh-CN/plugin.json +3 -0
- package/package.json +4 -6
- package/packages/builtin-tool-memory/src/client/Render/SearchUserMemory/index.tsx +3 -11
- package/packages/context-engine/src/engine/messages/MessagesEngine.ts +0 -13
- package/packages/context-engine/src/engine/messages/__tests__/MessagesEngine.test.ts +0 -25
- package/packages/database/src/models/__tests__/topics/topic.create.test.ts +3 -3
- package/packages/database/src/schemas/nextauth.ts +7 -2
- package/packages/utils/src/server/__tests__/auth.test.ts +1 -63
- package/packages/utils/src/server/auth.ts +8 -24
- package/scripts/_shared/checkDeprecatedAuth.js +99 -0
- package/scripts/clerk-to-betterauth/index.ts +8 -3
- package/scripts/nextauth-to-betterauth/_internal/config.ts +41 -0
- package/scripts/nextauth-to-betterauth/_internal/db.ts +32 -0
- package/scripts/nextauth-to-betterauth/_internal/env.ts +6 -0
- package/scripts/nextauth-to-betterauth/index.ts +226 -0
- package/scripts/nextauth-to-betterauth/verify.ts +188 -0
- package/scripts/prebuild.mts +66 -13
- package/scripts/serverLauncher/startServer.js +5 -5
- package/src/app/(backend)/api/auth/[...all]/route.ts +5 -23
- package/src/app/(backend)/api/webhooks/casdoor/route.ts +5 -5
- package/src/app/(backend)/api/webhooks/logto/route.ts +8 -8
- package/src/app/(backend)/middleware/auth/index.test.ts +8 -1
- package/src/app/(backend)/middleware/auth/index.ts +6 -15
- package/src/app/(backend)/middleware/auth/utils.test.ts +0 -32
- package/src/app/(backend)/middleware/auth/utils.ts +3 -8
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +8 -1
- package/src/app/(backend)/webapi/create-image/comfyui/route.ts +0 -1
- package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -1
- package/src/app/[variants]/(auth)/signin/SignInEmailStep.tsx +1 -1
- package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +4 -17
- package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/List/Item/index.tsx +1 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +34 -21
- package/src/app/[variants]/(main)/agent/features/Conversation/ConversationArea.tsx +4 -0
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/index.tsx +1 -0
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/InboxItem.tsx +19 -29
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/List.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/ModalProvider.tsx +1 -1
- package/src/app/[variants]/(main)/settings/profile/features/SSOProvidersList/index.tsx +12 -19
- package/src/app/[variants]/(main)/settings/profile/index.tsx +8 -14
- package/src/components/{NextAuth/AuthIcons.tsx → AuthIcons.tsx} +8 -10
- package/src/envs/auth.ts +12 -51
- package/src/envs/email.ts +3 -0
- package/src/envs/redis.ts +12 -54
- package/src/features/ChatInput/ChatInputProvider.tsx +22 -2
- package/src/features/ChatInput/InputEditor/index.tsx +14 -3
- package/src/features/ChatInput/store/initialState.ts +2 -0
- package/src/features/User/__tests__/PanelContent.test.tsx +0 -11
- package/src/features/User/__tests__/UserAvatar.test.tsx +1 -16
- package/src/layout/AuthProvider/index.tsx +1 -6
- package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -4
- package/src/libs/better-auth/define-config.ts +2 -0
- package/src/libs/better-auth/plugins/email-whitelist.test.ts +120 -0
- package/src/libs/better-auth/plugins/email-whitelist.ts +62 -0
- package/src/libs/next/config/define-config.ts +13 -1
- package/src/libs/next/proxy/define-config.ts +2 -75
- package/src/libs/oidc-provider/provider.test.ts +0 -4
- package/src/libs/redis/index.ts +0 -1
- package/src/libs/redis/manager.test.ts +9 -45
- package/src/libs/redis/manager.ts +2 -16
- package/src/libs/redis/redis.test.ts +2 -4
- package/src/libs/redis/redis.ts +2 -4
- package/src/libs/redis/types.ts +2 -24
- package/src/libs/redis/utils.test.ts +0 -10
- package/src/libs/redis/utils.ts +0 -19
- package/src/libs/trpc/lambda/context.test.ts +0 -13
- package/src/libs/trpc/lambda/context.ts +21 -59
- package/src/libs/trpc/middleware/userAuth.ts +1 -7
- package/src/libs/trusted-client/getSessionUser.ts +15 -35
- package/src/locales/default/plugin.ts +3 -0
- package/src/server/globalConfig/index.ts +1 -3
- package/src/server/modules/Mecha/ContextEngineering/__tests__/serverMessagesEngine.test.ts +0 -25
- package/src/server/routers/lambda/__tests__/user.test.ts +0 -48
- package/src/server/routers/lambda/user.ts +1 -12
- package/src/server/services/email/impls/nodemailer/index.ts +2 -2
- package/src/server/services/webhookUser/index.ts +88 -0
- package/src/services/chat/chat.test.ts +19 -19
- package/src/services/chat/index.ts +8 -3
- package/src/services/chat/mecha/agentConfigResolver.test.ts +72 -55
- package/src/services/chat/mecha/agentConfigResolver.ts +28 -4
- package/src/services/chat/mecha/contextEngineering.test.ts +21 -14
- package/src/services/chat/mecha/contextEngineering.ts +12 -0
- package/src/services/chat/types.ts +7 -1
- package/src/services/user/index.test.ts +0 -14
- package/src/services/user/index.ts +0 -4
- package/src/store/chat/agents/createAgentExecutors.ts +15 -4
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +1 -0
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -2
- package/src/store/user/slices/auth/action.test.ts +22 -126
- package/src/store/user/slices/auth/action.ts +32 -65
- package/src/store/user/slices/auth/initialState.ts +0 -3
- package/src/store/user/slices/auth/selectors.ts +0 -3
- package/tests/setup.ts +10 -0
- package/scripts/_shared/checkDeprecatedClerkEnv.js +0 -42
- package/src/app/(backend)/api/auth/adapter/route.ts +0 -137
- package/src/app/[variants]/(auth)/next-auth/error/AuthErrorPage.tsx +0 -40
- package/src/app/[variants]/(auth)/next-auth/error/page.tsx +0 -11
- package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +0 -167
- package/src/app/[variants]/(auth)/next-auth/signin/page.tsx +0 -11
- package/src/app/[variants]/(auth)/reset-password/layout.tsx +0 -12
- package/src/app/[variants]/(auth)/signin/layout.tsx +0 -12
- package/src/app/[variants]/(auth)/verify-email/layout.tsx +0 -12
- package/src/envs/auth.test.ts +0 -47
- package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +0 -44
- package/src/layout/AuthProvider/NextAuth/index.tsx +0 -17
- package/src/libs/next-auth/adapter/index.ts +0 -177
- package/src/libs/next-auth/auth.config.ts +0 -64
- package/src/libs/next-auth/index.ts +0 -20
- package/src/libs/next-auth/sso-providers/auth0.ts +0 -24
- package/src/libs/next-auth/sso-providers/authelia.ts +0 -39
- package/src/libs/next-auth/sso-providers/authentik.ts +0 -25
- package/src/libs/next-auth/sso-providers/casdoor.ts +0 -50
- package/src/libs/next-auth/sso-providers/cloudflare-zero-trust.ts +0 -34
- package/src/libs/next-auth/sso-providers/cognito.ts +0 -8
- package/src/libs/next-auth/sso-providers/feishu.ts +0 -83
- package/src/libs/next-auth/sso-providers/generic-oidc.ts +0 -38
- package/src/libs/next-auth/sso-providers/github.ts +0 -23
- package/src/libs/next-auth/sso-providers/google.ts +0 -18
- package/src/libs/next-auth/sso-providers/index.ts +0 -35
- package/src/libs/next-auth/sso-providers/keycloak.ts +0 -22
- package/src/libs/next-auth/sso-providers/logto.ts +0 -48
- package/src/libs/next-auth/sso-providers/microsoft-entra-id-helper.ts +0 -29
- package/src/libs/next-auth/sso-providers/microsoft-entra-id.ts +0 -19
- package/src/libs/next-auth/sso-providers/okta.ts +0 -22
- package/src/libs/next-auth/sso-providers/sso.config.ts +0 -8
- package/src/libs/next-auth/sso-providers/wechat.ts +0 -36
- package/src/libs/next-auth/sso-providers/zitadel.ts +0 -21
- package/src/libs/redis/upstash.test.ts +0 -158
- package/src/libs/redis/upstash.ts +0 -136
- package/src/server/services/nextAuthUser/index.ts +0 -318
- package/src/server/services/nextAuthUser/utils.ts +0 -62
- package/src/types/next-auth.d.ts +0 -26
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { type OIDCConfig, type OIDCUserConfig } from '@auth/core/providers';
|
|
2
|
-
|
|
3
|
-
import { CommonProviderConfig } from './sso.config';
|
|
4
|
-
|
|
5
|
-
interface LogtoProfile extends Record<string, any> {
|
|
6
|
-
email: string;
|
|
7
|
-
id: string;
|
|
8
|
-
name?: string;
|
|
9
|
-
picture: string;
|
|
10
|
-
sub: string;
|
|
11
|
-
username: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function LobeLogtoProvider(config: OIDCUserConfig<LogtoProfile>): OIDCConfig<LogtoProfile> {
|
|
15
|
-
return {
|
|
16
|
-
...CommonProviderConfig,
|
|
17
|
-
...config,
|
|
18
|
-
id: 'logto',
|
|
19
|
-
name: 'Logto',
|
|
20
|
-
profile(profile) {
|
|
21
|
-
// You can customize the user profile mapping here
|
|
22
|
-
return {
|
|
23
|
-
email: profile.email,
|
|
24
|
-
id: profile.sub,
|
|
25
|
-
image: profile.picture,
|
|
26
|
-
name: profile.name ?? profile.username ?? profile.email,
|
|
27
|
-
providerAccountId: profile.sub,
|
|
28
|
-
};
|
|
29
|
-
},
|
|
30
|
-
type: 'oidc',
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const provider = {
|
|
35
|
-
id: 'logto',
|
|
36
|
-
provider: LobeLogtoProvider({
|
|
37
|
-
authorization: {
|
|
38
|
-
params: { scope: 'openid offline_access profile email' },
|
|
39
|
-
},
|
|
40
|
-
// You can get the issuer value from the Logto Application Details page,
|
|
41
|
-
// in the field "Issuer endpoint"
|
|
42
|
-
clientId: process.env.AUTH_LOGTO_ID,
|
|
43
|
-
clientSecret: process.env.AUTH_LOGTO_SECRET,
|
|
44
|
-
issuer: process.env.AUTH_LOGTO_ISSUER,
|
|
45
|
-
}),
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export default provider;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { authEnv } from '@/envs/auth';
|
|
2
|
-
|
|
3
|
-
function getTenantId() {
|
|
4
|
-
return (
|
|
5
|
-
process.env.AUTH_MICROSOFT_ENTRA_ID_TENANT_ID ??
|
|
6
|
-
process.env.AUTH_AZURE_AD_TENANT_ID ??
|
|
7
|
-
authEnv.AZURE_AD_TENANT_ID
|
|
8
|
-
);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function getClientLoginBaseUrl() {
|
|
12
|
-
return process.env.AUTH_MICROSOFT_ENTRA_ID_BASE_URL ?? 'https://login.microsoftonline.com';
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function getIssuer() {
|
|
16
|
-
const issuer = process.env.MICROSOFT_ENTRA_ID_ISSUER;
|
|
17
|
-
if (issuer) {
|
|
18
|
-
return issuer;
|
|
19
|
-
}
|
|
20
|
-
const tenantId = getTenantId();
|
|
21
|
-
if (tenantId) {
|
|
22
|
-
// refs: https://github.com/nextauthjs/next-auth/discussions/9154#discussioncomment-10583104
|
|
23
|
-
return `${getClientLoginBaseUrl()}/${tenantId}/v2.0`;
|
|
24
|
-
} else {
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export { getIssuer as getMicrosoftEntraIdIssuer, getTenantId as getMicrosoftEntraIdTenantId };
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import MicrosoftEntraID from 'next-auth/providers/microsoft-entra-id';
|
|
2
|
-
|
|
3
|
-
import { getMicrosoftEntraIdIssuer } from './microsoft-entra-id-helper';
|
|
4
|
-
import { CommonProviderConfig } from './sso.config';
|
|
5
|
-
|
|
6
|
-
const provider = {
|
|
7
|
-
id: 'microsoft-entra-id',
|
|
8
|
-
provider: MicrosoftEntraID({
|
|
9
|
-
...CommonProviderConfig,
|
|
10
|
-
// Specify auth scope, at least include 'openid email'
|
|
11
|
-
// all scopes in Azure AD ref: https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc#openid-connect-scopes
|
|
12
|
-
authorization: { params: { scope: 'openid email profile' } },
|
|
13
|
-
clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID ?? process.env.AUTH_AZURE_AD_ID,
|
|
14
|
-
clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET ?? process.env.AUTH_AZURE_AD_SECRET,
|
|
15
|
-
issuer: getMicrosoftEntraIdIssuer(),
|
|
16
|
-
}),
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export default provider;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import Okta from 'next-auth/providers/okta';
|
|
2
|
-
|
|
3
|
-
import { CommonProviderConfig } from './sso.config';
|
|
4
|
-
|
|
5
|
-
const provider = {
|
|
6
|
-
id: 'okta',
|
|
7
|
-
provider: Okta({
|
|
8
|
-
...CommonProviderConfig,
|
|
9
|
-
authorization: { params: { scope: 'openid email profile' } },
|
|
10
|
-
profile(profile) {
|
|
11
|
-
return {
|
|
12
|
-
email: profile.email,
|
|
13
|
-
id: profile.sub,
|
|
14
|
-
image: profile.picture,
|
|
15
|
-
name: profile.name ?? profile.preferred_username,
|
|
16
|
-
providerAccountId: profile.sub,
|
|
17
|
-
};
|
|
18
|
-
},
|
|
19
|
-
}),
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export default provider;
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { type OAuth2Config } from '@auth/core/providers';
|
|
2
|
-
import type { Profile } from 'next-auth';
|
|
3
|
-
|
|
4
|
-
export const CommonProviderConfig = {
|
|
5
|
-
// Auth.js does not allow email account linking by default cause it's dangerous
|
|
6
|
-
// ref: https://authjs.dev/reference/core/providers#allowdangerousemailaccountlinking
|
|
7
|
-
allowDangerousEmailAccountLinking: true,
|
|
8
|
-
} satisfies Partial<OAuth2Config<Profile>>;
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import WeChat, { type WeChatProfile } from '@auth/core/providers/wechat';
|
|
2
|
-
|
|
3
|
-
import { CommonProviderConfig } from './sso.config';
|
|
4
|
-
|
|
5
|
-
const provider = {
|
|
6
|
-
id: 'wechat',
|
|
7
|
-
provider: WeChat({
|
|
8
|
-
...CommonProviderConfig,
|
|
9
|
-
clientId: process.env.AUTH_WECHAT_ID,
|
|
10
|
-
clientSecret: process.env.AUTH_WECHAT_SECRET,
|
|
11
|
-
platformType: 'WebsiteApp',
|
|
12
|
-
profile: (profile: WeChatProfile) => {
|
|
13
|
-
return {
|
|
14
|
-
email: null,
|
|
15
|
-
id: profile.unionid,
|
|
16
|
-
image: profile.headimgurl,
|
|
17
|
-
name: profile.nickname,
|
|
18
|
-
providerAccountId: profile.unionid,
|
|
19
|
-
};
|
|
20
|
-
},
|
|
21
|
-
style: { bg: '#fff', logo: 'https://authjs.dev/img/providers/wechat.svg', text: '#000' },
|
|
22
|
-
token: {
|
|
23
|
-
async conform(response: Response) {
|
|
24
|
-
const data = await response.json();
|
|
25
|
-
console.log('wechat data:', data);
|
|
26
|
-
return new Response(JSON.stringify({ ...data, token_type: 'bearer' }), {
|
|
27
|
-
headers: { 'Content-Type': 'application/json' },
|
|
28
|
-
});
|
|
29
|
-
},
|
|
30
|
-
params: { appid: process.env.AUTH_WECHAT_ID, secret: process.env.AUTH_WECHAT_SECRET },
|
|
31
|
-
url: 'https://api.weixin.qq.com/sns/oauth2/access_token',
|
|
32
|
-
},
|
|
33
|
-
}),
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export default provider;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import Zitadel from 'next-auth/providers/zitadel';
|
|
2
|
-
|
|
3
|
-
const provider = {
|
|
4
|
-
id: 'zitadel',
|
|
5
|
-
provider: Zitadel({
|
|
6
|
-
// Available scopes in ZITADEL: https://zitadel.com/docs/apis/openidoauth/scopes
|
|
7
|
-
authorization: { params: { scope: 'openid email profile' } },
|
|
8
|
-
// TODO(NextAuth): map unique user id to `providerAccountId` field
|
|
9
|
-
// profile(profile) {
|
|
10
|
-
// return {
|
|
11
|
-
// email: profile.email,
|
|
12
|
-
// image: profile.picture,
|
|
13
|
-
// name: profile.name,
|
|
14
|
-
// providerAccountId: profile.user_id,
|
|
15
|
-
// id: profile.user_id,
|
|
16
|
-
// };
|
|
17
|
-
// },
|
|
18
|
-
}),
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export default provider;
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
// @vitest-environment node
|
|
2
|
-
// NOTICE: here due to the reason we are using [`happy-dom`](https://github.com/lobehub/lobe-chat/blob/13753145557a9dede98b1f5489f93ac570ef2956/vitest.config.mts#L45)
|
|
3
|
-
// for Vitest environment, and in fact that this is a known bug for happy-dom not including
|
|
4
|
-
// Authorization header in fetch requests.
|
|
5
|
-
//
|
|
6
|
-
// Read more here: https://github.com/capricorn86/happy-dom/issues/1042#issuecomment-3585851354
|
|
7
|
-
import { Buffer } from 'node:buffer';
|
|
8
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
-
|
|
10
|
-
import { UpstashConfig } from './types';
|
|
11
|
-
|
|
12
|
-
const buildUpstashConfig = (): UpstashConfig | null => {
|
|
13
|
-
const url = process.env.UPSTASH_REDIS_REST_URL;
|
|
14
|
-
const token = process.env.UPSTASH_REDIS_REST_TOKEN;
|
|
15
|
-
|
|
16
|
-
if (!url || !token) return null;
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
enabled: true,
|
|
20
|
-
prefix: process.env.REDIS_PREFIX ?? 'lobe-chat-test',
|
|
21
|
-
provider: 'upstash',
|
|
22
|
-
token,
|
|
23
|
-
url,
|
|
24
|
-
};
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const loadUpstashProvider = async () => (await import('./upstash')).UpstashRedisProvider;
|
|
28
|
-
|
|
29
|
-
const createMockedProvider = async () => {
|
|
30
|
-
const mocks = {
|
|
31
|
-
mockSet: vi.fn().mockResolvedValue('OK'),
|
|
32
|
-
mockGet: vi.fn().mockResolvedValue('mock-value'),
|
|
33
|
-
mockDel: vi.fn().mockResolvedValue(1),
|
|
34
|
-
mockSetex: vi.fn().mockResolvedValue('OK'),
|
|
35
|
-
mockMset: vi.fn().mockResolvedValue('OK'),
|
|
36
|
-
mockHset: vi.fn().mockResolvedValue(1),
|
|
37
|
-
mockHdel: vi.fn().mockResolvedValue(1),
|
|
38
|
-
mockHgetall: vi.fn().mockResolvedValue({ a: '1' }),
|
|
39
|
-
mockPing: vi.fn().mockResolvedValue('PONG'),
|
|
40
|
-
mockExists: vi.fn().mockResolvedValue(1),
|
|
41
|
-
mockExpire: vi.fn().mockResolvedValue(1),
|
|
42
|
-
mockTtl: vi.fn().mockResolvedValue(50),
|
|
43
|
-
mockIncr: vi.fn().mockResolvedValue(2),
|
|
44
|
-
mockDecr: vi.fn().mockResolvedValue(0),
|
|
45
|
-
mockMget: vi.fn().mockResolvedValue(['a', 'b']),
|
|
46
|
-
mockHget: vi.fn().mockResolvedValue('field'),
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
vi.resetModules();
|
|
50
|
-
vi.doMock('@upstash/redis', () => {
|
|
51
|
-
class FakeRedis {
|
|
52
|
-
constructor(public config: any) {}
|
|
53
|
-
ping = mocks.mockPing;
|
|
54
|
-
set = mocks.mockSet;
|
|
55
|
-
get = mocks.mockGet;
|
|
56
|
-
del = mocks.mockDel;
|
|
57
|
-
setex = mocks.mockSetex;
|
|
58
|
-
exists = mocks.mockExists;
|
|
59
|
-
expire = mocks.mockExpire;
|
|
60
|
-
ttl = mocks.mockTtl;
|
|
61
|
-
incr = mocks.mockIncr;
|
|
62
|
-
decr = mocks.mockDecr;
|
|
63
|
-
mget = mocks.mockMget;
|
|
64
|
-
mset = mocks.mockMset;
|
|
65
|
-
hget = mocks.mockHget;
|
|
66
|
-
hset = mocks.mockHset;
|
|
67
|
-
hdel = mocks.mockHdel;
|
|
68
|
-
hgetall = mocks.mockHgetall;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return { Redis: FakeRedis };
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const UpstashRedisProvider = await loadUpstashProvider();
|
|
75
|
-
const provider = new UpstashRedisProvider({
|
|
76
|
-
enabled: true,
|
|
77
|
-
prefix: 'mock',
|
|
78
|
-
provider: 'upstash',
|
|
79
|
-
token: 'token',
|
|
80
|
-
url: 'https://example.upstash.io',
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
await provider.initialize();
|
|
84
|
-
|
|
85
|
-
return { mocks, provider };
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const shouldSkipIntegration = (error: unknown) =>
|
|
89
|
-
error instanceof Error &&
|
|
90
|
-
['ENOTFOUND', 'ECONNREFUSED', 'EAI_AGAIN', 'Connection is closed'].some((msg) =>
|
|
91
|
-
error.message.includes(msg),
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
afterEach(() => {
|
|
95
|
-
vi.clearAllMocks();
|
|
96
|
-
vi.resetModules();
|
|
97
|
-
vi.unmock('@upstash/redis');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe('integrated', (test) => {
|
|
101
|
-
const config = buildUpstashConfig();
|
|
102
|
-
if (!config) {
|
|
103
|
-
test.skip('UPSTASH_REDIS_REST_URL/TOKEN not provided; skip integrated upstash tests');
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
it('set -> get -> del roundtrip', async () => {
|
|
108
|
-
vi.unmock('@upstash/redis');
|
|
109
|
-
vi.resetModules();
|
|
110
|
-
|
|
111
|
-
const UpstashRedisProvider = await loadUpstashProvider();
|
|
112
|
-
const provider = new UpstashRedisProvider(config);
|
|
113
|
-
try {
|
|
114
|
-
await provider.initialize();
|
|
115
|
-
|
|
116
|
-
const key = `upstash:test:${Date.now()}`;
|
|
117
|
-
await provider.set(key, 'value', { ex: 60 });
|
|
118
|
-
expect(await provider.get(key)).toBe('value');
|
|
119
|
-
expect(await provider.del(key)).toBe(1);
|
|
120
|
-
} catch (error) {
|
|
121
|
-
if (shouldSkipIntegration(error)) {
|
|
122
|
-
// Remote Upstash Redis unavailable in current environment; treat as skipped.
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
throw error;
|
|
127
|
-
} finally {
|
|
128
|
-
await provider.disconnect();
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('mocked', () => {
|
|
134
|
-
it('normalizes buffer keys to strings', async () => {
|
|
135
|
-
const { mocks, provider } = await createMockedProvider();
|
|
136
|
-
|
|
137
|
-
const bufKey = Buffer.from('buffer-key');
|
|
138
|
-
await provider.set(bufKey, 'value');
|
|
139
|
-
await provider.hset(bufKey, 'field', 'value');
|
|
140
|
-
await provider.del(bufKey);
|
|
141
|
-
|
|
142
|
-
expect(mocks.mockSet).toHaveBeenCalledWith('mock:buffer-key', 'value', undefined);
|
|
143
|
-
expect(mocks.mockHset).toHaveBeenCalledWith('mock:buffer-key', { field: 'value' });
|
|
144
|
-
expect(mocks.mockDel).toHaveBeenCalledWith('mock:buffer-key');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('passes set options through to upstash client', async () => {
|
|
148
|
-
const { mocks, provider } = await createMockedProvider();
|
|
149
|
-
|
|
150
|
-
await provider.set('key', 'value', { ex: 10, nx: true, get: true });
|
|
151
|
-
|
|
152
|
-
expect(mocks.mockSet).toHaveBeenCalledWith('mock:key', 'value', {
|
|
153
|
-
ex: 10,
|
|
154
|
-
nx: true,
|
|
155
|
-
get: true,
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
});
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { Redis, type RedisConfigNodejs } from '@upstash/redis';
|
|
2
|
-
import { Buffer } from 'node:buffer';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
type BaseRedisProvider,
|
|
6
|
-
type RedisKey,
|
|
7
|
-
type RedisMSetArgument,
|
|
8
|
-
type RedisSetResult,
|
|
9
|
-
type RedisValue,
|
|
10
|
-
type SetOptions,
|
|
11
|
-
type UpstashConfig,
|
|
12
|
-
} from './types';
|
|
13
|
-
import {
|
|
14
|
-
buildUpstashSetOptions,
|
|
15
|
-
normalizeMsetValues,
|
|
16
|
-
normalizeRedisKey,
|
|
17
|
-
normalizeRedisKeys,
|
|
18
|
-
} from './utils';
|
|
19
|
-
|
|
20
|
-
export class UpstashRedisProvider implements BaseRedisProvider {
|
|
21
|
-
provider: 'upstash' = 'upstash';
|
|
22
|
-
private client: Redis;
|
|
23
|
-
private readonly prefix: string;
|
|
24
|
-
|
|
25
|
-
constructor(options: UpstashConfig | RedisConfigNodejs) {
|
|
26
|
-
const { prefix, ...clientOptions } = options as UpstashConfig & RedisConfigNodejs;
|
|
27
|
-
this.prefix = prefix ? `${prefix}:` : '';
|
|
28
|
-
this.client = new Redis({
|
|
29
|
-
...clientOptions,
|
|
30
|
-
automaticDeserialization: false,
|
|
31
|
-
} as RedisConfigNodejs);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Build a fully qualified key assuming the input was already normalized.
|
|
36
|
-
* Avoids re-running normalization when callers have normalized keys (e.g. mset).
|
|
37
|
-
*/
|
|
38
|
-
private addPrefixToKey(normalizedKey: string) {
|
|
39
|
-
return `${this.prefix}${normalizedKey}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
private buildKey(key: RedisKey) {
|
|
43
|
-
return this.addPrefixToKey(normalizeRedisKey(key));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private buildKeys(keys: RedisKey[]) {
|
|
47
|
-
return normalizeRedisKeys(keys).map((key) => `${this.prefix}${key}`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async initialize(): Promise<void> {
|
|
51
|
-
await this.client.ping();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async disconnect() {
|
|
55
|
-
// upstash client is stateless http, nothing to disconnect
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async get(key: RedisKey): Promise<string | null> {
|
|
59
|
-
return this.client.get(this.buildKey(key));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async set(key: RedisKey, value: RedisValue, options?: SetOptions): Promise<RedisSetResult> {
|
|
63
|
-
const res = await this.client.set(this.buildKey(key), value, buildUpstashSetOptions(options));
|
|
64
|
-
if (Buffer.isBuffer(res)) {
|
|
65
|
-
return res.toString();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return res;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async setex(key: RedisKey, seconds: number, value: RedisValue): Promise<'OK'> {
|
|
72
|
-
return this.client.setex(this.buildKey(key), seconds, value);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async del(...keys: RedisKey[]): Promise<number> {
|
|
76
|
-
return this.client.del(...this.buildKeys(keys));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async exists(...keys: RedisKey[]): Promise<number> {
|
|
80
|
-
return this.client.exists(...this.buildKeys(keys));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async expire(key: RedisKey, seconds: number): Promise<number> {
|
|
84
|
-
return this.client.expire(this.buildKey(key), seconds);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async ttl(key: RedisKey): Promise<number> {
|
|
88
|
-
return this.client.ttl(this.buildKey(key));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async incr(key: RedisKey): Promise<number> {
|
|
92
|
-
return this.client.incr(this.buildKey(key));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async decr(key: RedisKey): Promise<number> {
|
|
96
|
-
return this.client.decr(this.buildKey(key));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async mget(...keys: RedisKey[]): Promise<(string | null)[]> {
|
|
100
|
-
return this.client.mget(...this.buildKeys(keys));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async mset(values: RedisMSetArgument): Promise<'OK'> {
|
|
104
|
-
const normalized = normalizeMsetValues(values);
|
|
105
|
-
const prefixed = Object.entries(normalized).reduce<Record<string, RedisValue>>(
|
|
106
|
-
(acc, [key, value]) => {
|
|
107
|
-
acc[this.addPrefixToKey(key)] = value;
|
|
108
|
-
return acc;
|
|
109
|
-
},
|
|
110
|
-
{},
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
return this.client.mset(prefixed);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async hget(key: RedisKey, field: RedisKey): Promise<string | null> {
|
|
117
|
-
return this.client.hget(this.buildKey(key), normalizeRedisKey(field));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async hset(key: RedisKey, field: RedisKey, value: RedisValue): Promise<number> {
|
|
121
|
-
return this.client.hset(this.buildKey(key), { [normalizeRedisKey(field)]: value });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async hdel(key: RedisKey, ...fields: RedisKey[]): Promise<number> {
|
|
125
|
-
return this.client.hdel(this.buildKey(key), ...normalizeRedisKeys(fields));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async hgetall(key: RedisKey): Promise<Record<string, string>> {
|
|
129
|
-
const res = await this.client.hgetall(this.buildKey(key));
|
|
130
|
-
if (!res) {
|
|
131
|
-
return {};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return res as Record<string, string>;
|
|
135
|
-
}
|
|
136
|
-
}
|