@lobehub/lobehub 2.0.0-next.174 → 2.0.0-next.176

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +42 -0
  3. package/README.md +8 -8
  4. package/README.zh-CN.md +8 -8
  5. package/changelog/v1.json +14 -0
  6. package/docs/development/database-schema.dbml +26 -0
  7. package/next.config.ts +27 -0
  8. package/package.json +3 -1
  9. package/packages/agent-runtime/package.json +4 -1
  10. package/packages/const/package.json +5 -0
  11. package/packages/database/migrations/0064_add_agents_session_group_id.sql +7 -0
  12. package/packages/database/migrations/0065_add_passkey.sql +22 -0
  13. package/packages/database/migrations/0066_add_document_fields.sql +8 -0
  14. package/packages/database/migrations/meta/0064_snapshot.json +9143 -0
  15. package/packages/database/migrations/meta/0065_snapshot.json +9922 -0
  16. package/packages/database/migrations/meta/0066_snapshot.json +9962 -0
  17. package/packages/database/migrations/meta/_journal.json +21 -0
  18. package/packages/database/package.json +15 -0
  19. package/packages/database/src/core/migrations.json +64 -11
  20. package/packages/database/src/repositories/tableViewer/index.test.ts +1 -1
  21. package/packages/database/src/schemas/agent.ts +6 -0
  22. package/packages/database/src/schemas/betterAuth.ts +40 -1
  23. package/packages/database/src/schemas/file.ts +9 -3
  24. package/packages/electron-client-ipc/package.json +3 -0
  25. package/packages/model-bank/package.json +3 -0
  26. package/packages/model-runtime/package.json +18 -1
  27. package/packages/types/package.json +11 -0
  28. package/packages/utils/package.json +24 -2
  29. package/packages/web-crawler/package.json +1 -1
  30. package/public/.well-known/apple-app-site-association +5 -0
  31. package/public/.well-known/assetlinks.json +18 -0
  32. package/src/auth.ts +69 -1
  33. package/src/libs/better-auth/email-templates/index.ts +1 -0
  34. package/src/libs/better-auth/email-templates/verification-otp.ts +106 -0
  35. package/src/libs/better-auth/utils/config.ts +20 -1
@@ -448,6 +448,27 @@
448
448
  "when": 1766157362540,
449
449
  "tag": "0063_add_columns_for_several_tables",
450
450
  "breakpoints": true
451
+ },
452
+ {
453
+ "idx": 64,
454
+ "version": "7",
455
+ "when": 1766297832021,
456
+ "tag": "0064_add_agents_session_group_id",
457
+ "breakpoints": true
458
+ },
459
+ {
460
+ "idx": 65,
461
+ "version": "7",
462
+ "when": 1766408202688,
463
+ "tag": "0065_add_passkey",
464
+ "breakpoints": true
465
+ },
466
+ {
467
+ "idx": 66,
468
+ "version": "7",
469
+ "when": 1766474494249,
470
+ "tag": "0066_add_document_fields",
471
+ "breakpoints": true
451
472
  }
452
473
  ],
453
474
  "version": "6"
@@ -17,10 +17,25 @@
17
17
  "@lobechat/const": "workspace:*",
18
18
  "@lobechat/types": "workspace:*",
19
19
  "@lobechat/utils": "workspace:*",
20
+ "@lobehub/charts": "^2.1.2",
21
+ "@lobehub/chat-plugin-sdk": "^1.32.4",
22
+ "@neondatabase/serverless": "^1.0.2",
23
+ "@trpc/server": "^11.7.1",
24
+ "debug": "^4.4.3",
25
+ "drizzle-zod": "^0.5.1",
26
+ "lodash-es": "^4.17.21",
27
+ "model-bank": "workspace:*",
28
+ "next-auth": "5.0.0-beta.30",
29
+ "p-map": "^7.0.4",
20
30
  "random-words": "^2.0.1",
21
31
  "ts-md5": "^2.0.1",
32
+ "type-fest": "^5.2.0",
22
33
  "ws": "^8.18.3"
23
34
  },
35
+ "devDependencies": {
36
+ "dotenv": "^17.2.3",
37
+ "fake-indexeddb": "^6.2.5"
38
+ },
24
39
  "peerDependencies": {
25
40
  "@electric-sql/pglite": "^0.2.17",
26
41
  "dayjs": ">=1.11.19",
@@ -223,7 +223,10 @@
223
223
  "hash": "9646161fa041354714f823d726af27247bcd6e60fa3be5698c0d69f337a5700b"
224
224
  },
225
225
  {
226
- "sql": ["DROP TABLE \"user_budgets\";", "\nDROP TABLE \"user_subscriptions\";"],
226
+ "sql": [
227
+ "DROP TABLE \"user_budgets\";",
228
+ "\nDROP TABLE \"user_subscriptions\";"
229
+ ],
227
230
  "bps": true,
228
231
  "folderMillis": 1729699958471,
229
232
  "hash": "7dad43a2a25d1aec82124a4e53f8d82f8505c3073f23606c1dc5d2a4598eacf9"
@@ -295,7 +298,9 @@
295
298
  "hash": "845a692ceabbfc3caf252a97d3e19a213bc0c433df2689900135f9cfded2cf49"
296
299
  },
297
300
  {
298
- "sql": ["ALTER TABLE \"messages\" ADD COLUMN \"reasoning\" jsonb;"],
301
+ "sql": [
302
+ "ALTER TABLE \"messages\" ADD COLUMN \"reasoning\" jsonb;"
303
+ ],
299
304
  "bps": true,
300
305
  "folderMillis": 1737609172353,
301
306
  "hash": "2cb36ae4fcdd7b7064767e04bfbb36ae34518ff4bb1b39006f2dd394d1893868"
@@ -510,7 +515,9 @@
510
515
  "hash": "a7ccf007fd185ff922823148d1eae6fafe652fc98d2fd2793f84a84f29e93cd1"
511
516
  },
512
517
  {
513
- "sql": ["ALTER TABLE \"ai_providers\" ADD COLUMN \"config\" jsonb;"],
518
+ "sql": [
519
+ "ALTER TABLE \"ai_providers\" ADD COLUMN \"config\" jsonb;"
520
+ ],
514
521
  "bps": true,
515
522
  "folderMillis": 1749309388370,
516
523
  "hash": "39cea379f08ee4cb944875c0b67f7791387b508c2d47958bb4cd501ed1ef33eb"
@@ -628,7 +635,9 @@
628
635
  "hash": "1ba9b1f74ea13348da98d6fcdad7867ab4316ed565bf75d84d160c526cdac14b"
629
636
  },
630
637
  {
631
- "sql": ["ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"virtual\" boolean DEFAULT false;"],
638
+ "sql": [
639
+ "ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"virtual\" boolean DEFAULT false;"
640
+ ],
632
641
  "bps": true,
633
642
  "folderMillis": 1759116400580,
634
643
  "hash": "433ddae88e785f2db734e49a4c115eee93e60afe389f7919d66e5ba9aa159a37"
@@ -678,13 +687,17 @@
678
687
  "hash": "4bdc6505797d7a33b622498c138cfd47f637239f6905e1c484cd01d9d5f21d6b"
679
688
  },
680
689
  {
681
- "sql": ["ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"image\" jsonb;"],
690
+ "sql": [
691
+ "ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"image\" jsonb;"
692
+ ],
682
693
  "bps": true,
683
694
  "folderMillis": 1760108430562,
684
695
  "hash": "ce09b301abb80f6563abc2f526bdd20b4f69bae430f09ba2179b9e3bfec43067"
685
696
  },
686
697
  {
687
- "sql": ["ALTER TABLE \"documents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"],
698
+ "sql": [
699
+ "ALTER TABLE \"documents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"
700
+ ],
688
701
  "bps": true,
689
702
  "folderMillis": 1761554153406,
690
703
  "hash": "bf2f21293e90e11cf60a784cf3ec219eafa95f7545d7d2f9d1449c0b0949599a"
@@ -764,13 +777,17 @@
764
777
  "hash": "923ccbdf46c32be9a981dabd348e6923b4a365444241e9b8cc174bf5b914cbc5"
765
778
  },
766
779
  {
767
- "sql": ["ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"market_identifier\" text;\n"],
780
+ "sql": [
781
+ "ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"market_identifier\" text;\n"
782
+ ],
768
783
  "bps": true,
769
784
  "folderMillis": 1762870034882,
770
785
  "hash": "4178aacb4b8892b7fd15d29209bbf9b1d1f9d7c406ba796f27542c0bcd919680"
771
786
  },
772
787
  {
773
- "sql": ["ALTER TABLE \"message_plugins\" ADD COLUMN IF NOT EXISTS \"intervention\" jsonb;\n"],
788
+ "sql": [
789
+ "ALTER TABLE \"message_plugins\" ADD COLUMN IF NOT EXISTS \"intervention\" jsonb;\n"
790
+ ],
774
791
  "bps": true,
775
792
  "folderMillis": 1762911968658,
776
793
  "hash": "552a032cc0e595277232e70b5f9338658585bafe9481ae8346a5f322b673a68b"
@@ -799,7 +816,9 @@
799
816
  "hash": "f823b521f4d25e5dc5ab238b372727d2d2d7f0aed27b5eabc8a9608ce4e50568"
800
817
  },
801
818
  {
802
- "sql": ["ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"],
819
+ "sql": [
820
+ "ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"editor_data\" jsonb;"
821
+ ],
803
822
  "bps": true,
804
823
  "folderMillis": 1764215503726,
805
824
  "hash": "4188893a9083b3c7baebdbad0dd3f9d9400ede7584ca2394f5c64305dc9ec7b0"
@@ -840,7 +859,9 @@
840
859
  "hash": "2c103eee82bdf329944fb622dd9c2b9f20df80eb54f23eb9254d2285de413099"
841
860
  },
842
861
  {
843
- "sql": ["ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"market\" jsonb;"],
862
+ "sql": [
863
+ "ALTER TABLE \"user_settings\" ADD COLUMN IF NOT EXISTS \"market\" jsonb;"
864
+ ],
844
865
  "bps": true,
845
866
  "folderMillis": 1764335703306,
846
867
  "hash": "28c0d738c0b1fdf5fd871363be1a1477b4accbabdc140fe8dc6e9b339aae2c89"
@@ -1023,5 +1044,37 @@
1023
1044
  "bps": true,
1024
1045
  "folderMillis": 1766157362540,
1025
1046
  "hash": "7a8ee107778222390e676951173baa81bfa09dd47216a8467575fca54915172c"
1047
+ },
1048
+ {
1049
+ "sql": [
1050
+ "ALTER TABLE \"agents\" ADD COLUMN IF NOT EXISTS \"session_group_id\" text;",
1051
+ "\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'agents_session_group_id_session_groups_id_fk') THEN\n ALTER TABLE \"agents\" ADD CONSTRAINT \"agents_session_group_id_session_groups_id_fk\" FOREIGN KEY (\"session_group_id\") REFERENCES \"public\".\"session_groups\"(\"id\") ON DELETE set null ON UPDATE no action;\n END IF;\nEND $$;",
1052
+ "\nCREATE INDEX IF NOT EXISTS \"agents_session_group_id_idx\" ON \"agents\" USING btree (\"session_group_id\");\n"
1053
+ ],
1054
+ "bps": true,
1055
+ "folderMillis": 1766297832021,
1056
+ "hash": "431a620396060130c46d6174d4bef3a517a0872aff8d19f3044bd9e7dec78ba5"
1057
+ },
1058
+ {
1059
+ "sql": [
1060
+ "CREATE TABLE IF NOT EXISTS \"passkey\" (\n\t\"aaguid\" text,\n\t\"backedUp\" boolean,\n\t\"counter\" integer,\n\t\"createdAt\" timestamp DEFAULT now(),\n\t\"credentialID\" text NOT NULL,\n\t\"deviceType\" text,\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"name\" text,\n\t\"publicKey\" text NOT NULL,\n\t\"transports\" text,\n\t\"userId\" text NOT NULL\n);\n",
1061
+ "\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'passkey_userId_users_id_fk') THEN\n ALTER TABLE \"passkey\" ADD CONSTRAINT \"passkey_userId_users_id_fk\" FOREIGN KEY (\"userId\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\n END IF;\nEND $$;\n",
1062
+ "\t\nCREATE UNIQUE INDEX IF NOT EXISTS \"passkey_credential_id_unique\" ON \"passkey\" USING btree (\"credentialID\");",
1063
+ "\nCREATE INDEX IF NOT EXISTS \"passkey_user_id_idx\" ON \"passkey\" USING btree (\"userId\");"
1064
+ ],
1065
+ "bps": true,
1066
+ "folderMillis": 1766408202688,
1067
+ "hash": "1ae24d25f4b0ee7f38353b6d7fc6be4ae1171c105eb329d8aa97888abc85373b"
1068
+ },
1069
+ {
1070
+ "sql": [
1071
+ "ALTER TABLE \"documents\" ADD COLUMN IF NOT EXISTS \"description\" text;",
1072
+ "\nALTER TABLE \"documents\" ADD COLUMN IF NOT EXISTS \"knowledge_base_id\" text;",
1073
+ "\nDO $$ BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'documents_knowledge_base_id_knowledge_bases_id_fk') THEN\n ALTER TABLE \"documents\" ADD CONSTRAINT \"documents_knowledge_base_id_knowledge_bases_id_fk\" FOREIGN KEY (\"knowledge_base_id\") REFERENCES \"public\".\"knowledge_bases\"(\"id\") ON DELETE set null ON UPDATE no action;\n END IF;\nEND $$;",
1074
+ "\nCREATE INDEX IF NOT EXISTS \"documents_knowledge_base_id_idx\" ON \"documents\" USING btree (\"knowledge_base_id\");\n"
1075
+ ],
1076
+ "bps": true,
1077
+ "folderMillis": 1766474494249,
1078
+ "hash": "42d46c25f68aa3d6ab46b7c15734d984a9a8452e8058e98de79f4d675a38e28f"
1026
1079
  }
1027
- ]
1080
+ ]
@@ -23,7 +23,7 @@ describe('TableViewerRepo', () => {
23
23
  it('should return all tables with counts', async () => {
24
24
  const result = await repo.getAllTables();
25
25
 
26
- expect(result.length).toEqual(72);
26
+ expect(result.length).toEqual(73);
27
27
  expect(result[0]).toEqual({ name: 'accounts', count: 0, type: 'BASE TABLE' });
28
28
  });
29
29
 
@@ -15,6 +15,7 @@ import { createInsertSchema } from 'drizzle-zod';
15
15
  import { idGenerator, randomSlug } from '../utils/idGenerator';
16
16
  import { timestamps } from './_helpers';
17
17
  import { files, knowledgeBases } from './file';
18
+ import { sessionGroups } from './session';
18
19
  import { users } from './user';
19
20
 
20
21
  // Agent table is the main table for storing agents
@@ -60,6 +61,10 @@ export const agents = pgTable(
60
61
  openingMessage: text('opening_message'),
61
62
  openingQuestions: text('opening_questions').array().default([]),
62
63
 
64
+ sessionGroupId: text('session_group_id').references(() => sessionGroups.id, {
65
+ onDelete: 'set null',
66
+ }),
67
+
63
68
  ...timestamps,
64
69
  },
65
70
  (t) => [
@@ -68,6 +73,7 @@ export const agents = pgTable(
68
73
  index('agents_user_id_idx').on(t.userId),
69
74
  index('agents_title_idx').on(t.title),
70
75
  index('agents_description_idx').on(t.description),
76
+ index('agents_session_group_id_idx').on(t.sessionGroupId),
71
77
  ],
72
78
  );
73
79
 
@@ -1,5 +1,13 @@
1
1
  import { relations } from 'drizzle-orm';
2
- import { index, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
2
+ import {
3
+ boolean,
4
+ index,
5
+ integer,
6
+ pgTable,
7
+ text,
8
+ timestamp,
9
+ uniqueIndex,
10
+ } from 'drizzle-orm/pg-core';
3
11
 
4
12
  import { users } from './user';
5
13
 
@@ -92,8 +100,32 @@ export const twoFactor = pgTable(
92
100
  ],
93
101
  );
94
102
 
103
+ export const passkey = pgTable(
104
+ 'passkey',
105
+ {
106
+ aaguid: text('aaguid'),
107
+ backedUp: boolean('backedUp'),
108
+ counter: integer('counter'),
109
+ createdAt: timestamp('createdAt').defaultNow(),
110
+ credentialID: text('credentialID').notNull(),
111
+ deviceType: text('deviceType'),
112
+ id: text('id').primaryKey(),
113
+ name: text('name'),
114
+ publicKey: text('publicKey').notNull(),
115
+ transports: text('transports'),
116
+ userId: text('userId')
117
+ .notNull()
118
+ .references(() => users.id, { onDelete: 'cascade' }),
119
+ },
120
+ (table) => [
121
+ uniqueIndex('passkey_credential_id_unique').on(table.credentialID),
122
+ index('passkey_user_id_idx').on(table.userId),
123
+ ],
124
+ );
125
+
95
126
  export const usersRelations = relations(users, ({ many }) => ({
96
127
  accounts: many(account),
128
+ passkeys: many(passkey),
97
129
  sessions: many(session),
98
130
  twoFactors: many(twoFactor),
99
131
  }));
@@ -118,3 +150,10 @@ export const twoFactorRelations = relations(twoFactor, ({ one }) => ({
118
150
  references: [users.id],
119
151
  }),
120
152
  }));
153
+
154
+ export const passkeysRelations = relations(passkey, ({ one }) => ({
155
+ users: one(users, {
156
+ fields: [passkey.userId],
157
+ references: [users.id],
158
+ }),
159
+ }));
@@ -18,7 +18,7 @@ import { createInsertSchema } from 'drizzle-zod';
18
18
  import { LobeDocumentPage } from '@/types/document';
19
19
  import { FileSource } from '@/types/files';
20
20
 
21
- import { idGenerator } from '../utils/idGenerator';
21
+ import { idGenerator, randomSlug } from '../utils/idGenerator';
22
22
  import { accessedAt, createdAt, timestamps } from './_helpers';
23
23
  import { asyncTasks } from './asyncTask';
24
24
  import { users } from './user';
@@ -55,6 +55,7 @@ export const documents = pgTable(
55
55
 
56
56
  // Basic information
57
57
  title: text('title'),
58
+ description: text('description'),
58
59
  content: text('content'),
59
60
 
60
61
  // Special type: custom/folder
@@ -72,13 +73,17 @@ export const documents = pgTable(
72
73
  pages: jsonb('pages').$type<LobeDocumentPage[]>(),
73
74
 
74
75
  // Source type
75
- sourceType: text('source_type', { enum: ['file', 'web', 'api'] }).notNull(),
76
+ sourceType: text('source_type', { enum: ['file', 'web', 'api', 'topic'] }).notNull(),
76
77
  source: text('source').notNull(), // File path or web URL
77
78
 
78
79
  // Associated file (optional)
79
80
  // forward reference needs AnyPgColumn to avoid circular type inference
80
81
  // eslint-disable-next-line @typescript-eslint/no-use-before-define
81
82
  fileId: text('file_id').references((): AnyPgColumn => files.id, { onDelete: 'set null' }),
83
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
84
+ knowledgeBaseId: text('knowledge_base_id').references(() => knowledgeBases.id, {
85
+ onDelete: 'set null',
86
+ }),
82
87
 
83
88
  // Parent document (for folder hierarchy structure)
84
89
  parentId: varchar('parent_id', { length: 255 }).references((): AnyPgColumn => documents.id, {
@@ -93,7 +98,7 @@ export const documents = pgTable(
93
98
 
94
99
  editorData: jsonb('editor_data').$type<Record<string, any>>(),
95
100
 
96
- slug: varchar('slug', { length: 255 }),
101
+ slug: varchar('slug', { length: 255 }).$defaultFn(() => randomSlug(3)),
97
102
 
98
103
  // Timestamps
99
104
  ...timestamps,
@@ -105,6 +110,7 @@ export const documents = pgTable(
105
110
  index('documents_user_id_idx').on(table.userId),
106
111
  index('documents_file_id_idx').on(table.fileId),
107
112
  index('documents_parent_id_idx').on(table.parentId),
113
+ index('documents_knowledge_base_id_idx').on(table.knowledgeBaseId),
108
114
  uniqueIndex('documents_client_id_user_id_unique').on(table.clientId, table.userId),
109
115
  uniqueIndex('documents_slug_user_id_unique')
110
116
  .on(table.slug, table.userId)
@@ -4,6 +4,9 @@
4
4
  "private": true,
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
7
+ "devDependencies": {
8
+ "electron": "^34.0.0"
9
+ },
7
10
  "peerDependencies": {
8
11
  "react": "^19.2.0"
9
12
  }
@@ -80,6 +80,9 @@
80
80
  "test:coverage": "vitest --coverage --silent='passed-only'"
81
81
  },
82
82
  "dependencies": {
83
+ "type-fest": "^5.2.0"
84
+ },
85
+ "peerDependencies": {
83
86
  "zod": "^3.25.76"
84
87
  }
85
88
  }
@@ -12,15 +12,32 @@
12
12
  "test:update": "vitest -u"
13
13
  },
14
14
  "dependencies": {
15
+ "@anthropic-ai/sdk": "^0.67.1",
15
16
  "@aws-sdk/client-bedrock-runtime": "^3.941.0",
17
+ "@azure-rest/ai-inference": "1.0.0-beta.5",
18
+ "@azure/core-auth": "^1.10.1",
19
+ "@fal-ai/client": "^1.7.2",
20
+ "@google/genai": "^1.29.0",
16
21
  "@huggingface/inference": "^4.13.4",
17
22
  "@lobechat/const": "workspace:*",
18
23
  "@lobechat/types": "workspace:*",
19
24
  "@lobechat/utils": "workspace:*",
20
25
  "async-retry": "^1.3.3",
26
+ "dayjs": "^1.11.19",
21
27
  "debug": "^4.4.3",
28
+ "immer": "^10.2.0",
29
+ "langfuse": "^3.38.6",
30
+ "langfuse-core": "^3.38.6",
31
+ "lodash-es": "^4.17.21",
22
32
  "model-bank": "workspace:*",
33
+ "nanoid": "^5.1.6",
34
+ "ollama": "^0.6.2",
23
35
  "openai": "^4.104.0",
24
- "replicate": "^1.4.0"
36
+ "replicate": "^1.4.0",
37
+ "type-fest": "^5.2.0",
38
+ "url-join": "^5.0.0"
39
+ },
40
+ "peerDependencies": {
41
+ "zod": "^3.25.76"
25
42
  }
26
43
  }
@@ -4,9 +4,20 @@
4
4
  "private": true,
5
5
  "main": "./src/index.ts",
6
6
  "dependencies": {
7
+ "@lobechat/model-runtime": "workspace:*",
8
+ "@lobechat/python-interpreter": "workspace:*",
7
9
  "@lobechat/web-crawler": "workspace:*",
8
10
  "@lobehub/chat-plugin-sdk": "^1.32.4",
11
+ "@lobehub/market-sdk": "beta",
12
+ "@lobehub/market-types": "^1.11.4",
13
+ "@lobehub/ui": "^2.13.8",
14
+ "model-bank": "workspace:*",
15
+ "react": "19.2.0",
9
16
  "type-fest": "^4.41.0",
17
+ "zod": "^3.25.76",
18
+ "zustand": "5.0.4"
19
+ },
20
+ "peerDependencies": {
10
21
  "zod": "^3.25.76"
11
22
  }
12
23
  }
@@ -16,10 +16,32 @@
16
16
  "dependencies": {
17
17
  "@lobechat/const": "workspace:*",
18
18
  "@lobechat/types": "workspace:*",
19
+ "@lobehub/chat-plugin-sdk": "^1.32.4",
20
+ "@vercel/functions": "^3.3.0",
21
+ "brotli-wasm": "^3.0.1",
22
+ "chroma-js": "^3.1.2",
23
+ "countries-and-timezones": "^3.8.0",
19
24
  "dayjs": "^1.11.19",
20
- "dompurify": "^3.3.0"
25
+ "debug": "^4.4.3",
26
+ "dompurify": "^3.3.0",
27
+ "fast-deep-equal": "^3.1.3",
28
+ "lodash-es": "^4.17.21",
29
+ "mime": "^4.1.0",
30
+ "model-bank": "workspace:*",
31
+ "nanoid": "^5.1.6",
32
+ "next": "^16.0.1",
33
+ "numeral": "^2.0.6",
34
+ "pure-rand": "^7.0.1",
35
+ "remark": "^15.0.1",
36
+ "remark-gfm": "^4.0.1",
37
+ "remark-html": "^16.0.1",
38
+ "ssrf-safe-fetch": "workspace:*",
39
+ "tokenx": "^1.2.1",
40
+ "ua-parser-js": "^1.0.41",
41
+ "uuid": "^11.1.0",
42
+ "yaml": "^2.8.1"
21
43
  },
22
44
  "devDependencies": {
23
- "vitest-canvas-mock": "^0.3.3"
45
+ "vitest-canvas-mock": "^1.1.3"
24
46
  }
25
47
  }
@@ -12,8 +12,8 @@
12
12
  "@mozilla/readability": "^0.6.0",
13
13
  "happy-dom": "^20.0.11",
14
14
  "node-html-markdown": "^1.3.0",
15
- "ssrf-safe-fetch": "workspace:*",
16
15
  "query-string": "^9.3.1",
16
+ "ssrf-safe-fetch": "workspace:*",
17
17
  "url-join": "^5"
18
18
  }
19
19
  }
@@ -0,0 +1,5 @@
1
+ {
2
+ "webcredentials": {
3
+ "apps": ["4684H589ZU.com.lobehub.app"]
4
+ }
5
+ }
@@ -0,0 +1,18 @@
1
+ [
2
+ {
3
+ "relation": [
4
+ "delegate_permission/common.handle_all_urls",
5
+ "delegate_permission/common.get_login_creds"
6
+ ],
7
+ "target": {
8
+ "namespace": "android_app",
9
+ "package_name": "com.lobehub.app",
10
+ "sha256_cert_fingerprints": [
11
+ "D7:54:DB:A3:78:D5:8B:8F:20:01:ED:7B:9B:18:D3:B0:5B:D1:22:AA:97:2B:59:E1:A6:8E:31:24:21:44:0D:2B",
12
+ "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C",
13
+ "1B:21:38:5D:72:40:65:F5:16:20:1D:C9:D2:6B:04:63:C3:33:F1:97:AB:6A:06:66:0E:3E:F0:7E:60:82:7E:E7",
14
+ "1B:BE:D4:A0:AE:43:56:E5:58:01:74:C4:B9:A0:0B:0E:5A:B9:5E:0F:A9:C0:65:18:68:CF:1F:AA:3E:8F:4F:DB"
15
+ ]
16
+ }
17
+ }
18
+ ]
package/src/auth.ts CHANGED
@@ -1,15 +1,19 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
2
+ import { expo } from '@better-auth/expo';
3
+ import { passkey } from '@better-auth/passkey';
2
4
  import { createNanoId, idGenerator, serverDB } from '@lobechat/database';
5
+ import * as schema from '@lobechat/database/schemas';
3
6
  import { emailHarmony } from 'better-auth-harmony';
4
7
  import { drizzleAdapter } from 'better-auth/adapters/drizzle';
5
8
  import { betterAuth } from 'better-auth/minimal';
6
- import { admin, genericOAuth, magicLink } from 'better-auth/plugins';
9
+ import { admin, emailOTP, genericOAuth, magicLink } from 'better-auth/plugins';
7
10
 
8
11
  import { authEnv } from '@/envs/auth';
9
12
  import {
10
13
  getMagicLinkEmailTemplate,
11
14
  getResetPasswordEmailTemplate,
12
15
  getVerificationEmailTemplate,
16
+ getVerificationOTPEmailTemplate,
13
17
  } from '@/libs/better-auth/email-templates';
14
18
  import { initBetterAuthSSOProviders } from '@/libs/better-auth/sso';
15
19
  import { createSecondaryStorage, getTrustedOrigins } from '@/libs/better-auth/utils/config';
@@ -20,7 +24,34 @@ import { UserService } from '@/server/services/user';
20
24
  // Email verification link expiration time (in seconds)
21
25
  // Default is 1 hour (3600 seconds) as per Better Auth documentation
22
26
  const VERIFICATION_LINK_EXPIRES_IN = 3600;
27
+
28
+ /**
29
+ * Safely extract hostname from AUTH_URL for passkey rpID.
30
+ * Returns undefined if AUTH_URL is not set (e.g., in e2e tests).
31
+ */
32
+ const getPasskeyRpID = (): string | undefined => {
33
+ if (!authEnv.NEXT_PUBLIC_AUTH_URL) return undefined;
34
+ try {
35
+ return new URL(authEnv.NEXT_PUBLIC_AUTH_URL).hostname;
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ };
40
+
41
+ /**
42
+ * Get passkey origins array.
43
+ * Returns undefined if AUTH_URL is not set (e.g., in e2e tests).
44
+ */
45
+ const getPasskeyOrigins = (): string[] | undefined => {
46
+ if (!authEnv.NEXT_PUBLIC_AUTH_URL) return undefined;
47
+ return [
48
+ // Web origin
49
+ authEnv.NEXT_PUBLIC_AUTH_URL,
50
+ ];
51
+ };
23
52
  const MAGIC_LINK_EXPIRES_IN = 900;
53
+ // OTP expiration time (in seconds) - 5 minutes for mobile OTP verification
54
+ const OTP_EXPIRES_IN = 300;
24
55
  const enableMagicLink = authEnv.NEXT_PUBLIC_ENABLE_MAGIC_LINK;
25
56
  const enabledSSOProviders = parseSSOProviders(authEnv.AUTH_SSO_PROVIDERS);
26
57
 
@@ -85,6 +116,8 @@ export const auth = betterAuth({
85
116
  },
86
117
  database: drizzleAdapter(serverDB, {
87
118
  provider: 'pg',
119
+ // experimental joins feature needs schema to pass full relation
120
+ schema,
88
121
  }),
89
122
  secondaryStorage: createSecondaryStorage(),
90
123
  /**
@@ -149,8 +182,43 @@ export const auth = betterAuth({
149
182
  },
150
183
  },
151
184
  plugins: [
185
+ expo(),
152
186
  emailHarmony({ allowNormalizedSignin: false }),
153
187
  admin(),
188
+ // Email OTP plugin for mobile verification
189
+ emailOTP({
190
+ expiresIn: OTP_EXPIRES_IN,
191
+ otpLength: 6,
192
+ allowedAttempts: 3,
193
+ // Don't automatically send OTP on sign up - let mobile client manually trigger it
194
+ sendVerificationOnSignUp: false,
195
+ async sendVerificationOTP({ email, otp }) {
196
+ const emailService = new EmailService();
197
+
198
+ // For all OTP types, use the same template
199
+ // userName is optional and will be null since we don't have user context here
200
+ const template = getVerificationOTPEmailTemplate({
201
+ expiresInSeconds: OTP_EXPIRES_IN,
202
+ otp,
203
+ userName: null,
204
+ });
205
+
206
+ await emailService.sendMail({
207
+ to: email,
208
+ ...template,
209
+ });
210
+ },
211
+ }),
212
+ passkey({
213
+ rpName: 'LobeHub',
214
+ // Extract rpID from auth URL (e.g., 'lobehub.com' from 'https://lobehub.com')
215
+ // Returns undefined if AUTH_URL is not set (e.g., in e2e tests)
216
+ rpID: getPasskeyRpID(),
217
+ // Support multiple origins: web + Android APK key hashes
218
+ // Android origin format: android:apk-key-hash:<base64url-sha256-fingerprint>
219
+ // Returns undefined if AUTH_URL is not set (e.g., in e2e tests)
220
+ origin: getPasskeyOrigins(),
221
+ }),
154
222
  ...(genericOAuthProviders.length > 0
155
223
  ? [
156
224
  genericOAuth({
@@ -1,3 +1,4 @@
1
1
  export { getMagicLinkEmailTemplate } from './magic-link';
2
2
  export { getResetPasswordEmailTemplate } from './reset-password';
3
3
  export { getVerificationEmailTemplate } from './verification';
4
+ export { getVerificationOTPEmailTemplate } from './verification-otp';