@lobehub/chat 1.99.6 → 1.100.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.
Files changed (92) hide show
  1. package/.github/workflows/desktop-pr-build.yml +3 -3
  2. package/.github/workflows/release-desktop-beta.yml +3 -3
  3. package/CHANGELOG.md +25 -0
  4. package/apps/desktop/package.json +5 -2
  5. package/apps/desktop/src/main/controllers/AuthCtr.ts +310 -111
  6. package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +1 -1
  7. package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +50 -3
  8. package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +188 -23
  9. package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +37 -18
  10. package/apps/desktop/src/main/types/store.ts +1 -0
  11. package/apps/desktop/src/preload/electronApi.ts +2 -1
  12. package/apps/desktop/src/preload/streamer.ts +58 -0
  13. package/changelog/v1.json +9 -0
  14. package/docs/development/database-schema.dbml +9 -0
  15. package/locales/ar/electron.json +3 -0
  16. package/locales/ar/oauth.json +8 -4
  17. package/locales/bg-BG/electron.json +3 -0
  18. package/locales/bg-BG/oauth.json +8 -4
  19. package/locales/de-DE/electron.json +3 -0
  20. package/locales/de-DE/oauth.json +9 -5
  21. package/locales/en-US/electron.json +3 -0
  22. package/locales/en-US/oauth.json +8 -4
  23. package/locales/es-ES/electron.json +3 -0
  24. package/locales/es-ES/oauth.json +9 -5
  25. package/locales/fa-IR/electron.json +3 -0
  26. package/locales/fa-IR/oauth.json +8 -4
  27. package/locales/fr-FR/electron.json +3 -0
  28. package/locales/fr-FR/oauth.json +8 -4
  29. package/locales/it-IT/electron.json +3 -0
  30. package/locales/it-IT/oauth.json +9 -5
  31. package/locales/ja-JP/electron.json +3 -0
  32. package/locales/ja-JP/oauth.json +8 -4
  33. package/locales/ko-KR/electron.json +3 -0
  34. package/locales/ko-KR/oauth.json +8 -4
  35. package/locales/nl-NL/electron.json +3 -0
  36. package/locales/nl-NL/oauth.json +9 -5
  37. package/locales/pl-PL/electron.json +3 -0
  38. package/locales/pl-PL/oauth.json +8 -4
  39. package/locales/pt-BR/electron.json +3 -0
  40. package/locales/pt-BR/oauth.json +8 -4
  41. package/locales/ru-RU/electron.json +3 -0
  42. package/locales/ru-RU/oauth.json +8 -4
  43. package/locales/tr-TR/electron.json +3 -0
  44. package/locales/tr-TR/oauth.json +8 -4
  45. package/locales/vi-VN/electron.json +3 -0
  46. package/locales/vi-VN/oauth.json +9 -5
  47. package/locales/zh-CN/electron.json +3 -0
  48. package/locales/zh-CN/oauth.json +8 -4
  49. package/locales/zh-TW/electron.json +3 -0
  50. package/locales/zh-TW/oauth.json +8 -4
  51. package/package.json +3 -3
  52. package/packages/electron-client-ipc/src/dispatch.ts +14 -2
  53. package/packages/electron-client-ipc/src/index.ts +1 -0
  54. package/packages/electron-client-ipc/src/streamInvoke.ts +62 -0
  55. package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +5 -0
  56. package/packages/electron-client-ipc/src/utils/headers.ts +27 -0
  57. package/packages/electron-client-ipc/src/utils/request.ts +28 -0
  58. package/src/app/(backend)/oidc/callback/desktop/route.ts +58 -0
  59. package/src/app/(backend)/oidc/handoff/route.ts +46 -0
  60. package/src/app/[variants]/oauth/callback/error/page.tsx +55 -0
  61. package/src/app/[variants]/oauth/callback/layout.tsx +12 -0
  62. package/src/app/[variants]/oauth/callback/loading.tsx +3 -0
  63. package/src/app/[variants]/oauth/{consent/[uid] → callback}/success/page.tsx +10 -1
  64. package/src/app/[variants]/oauth/consent/[uid]/Consent.tsx +7 -1
  65. package/src/database/client/migrations.json +8 -0
  66. package/src/database/migrations/0028_oauth_handoffs.sql +8 -0
  67. package/src/database/migrations/meta/0028_snapshot.json +6055 -0
  68. package/src/database/migrations/meta/_journal.json +7 -0
  69. package/src/database/models/oauthHandoff.ts +94 -0
  70. package/src/database/repositories/tableViewer/index.test.ts +1 -1
  71. package/src/database/schemas/oidc.ts +46 -0
  72. package/src/features/ElectronTitlebar/Connection/Waiting.tsx +59 -115
  73. package/src/features/ElectronTitlebar/Connection/WaitingAnim.tsx +114 -0
  74. package/src/libs/oidc-provider/config.ts +16 -17
  75. package/src/libs/oidc-provider/jwt.ts +135 -0
  76. package/src/libs/oidc-provider/provider.ts +22 -38
  77. package/src/libs/trpc/client/async.ts +1 -2
  78. package/src/libs/trpc/client/edge.ts +1 -2
  79. package/src/libs/trpc/client/lambda.ts +1 -1
  80. package/src/libs/trpc/client/tools.ts +1 -2
  81. package/src/libs/trpc/lambda/context.ts +9 -16
  82. package/src/locales/default/electron.ts +3 -0
  83. package/src/locales/default/oauth.ts +8 -4
  84. package/src/middleware.ts +10 -4
  85. package/src/server/services/oidc/index.ts +0 -71
  86. package/src/services/chat.ts +5 -1
  87. package/src/services/electron/remoteServer.ts +0 -7
  88. package/src/{libs/trpc/client/helpers → utils/electron}/desktopRemoteRPCFetch.ts +22 -7
  89. package/src/utils/server/auth.ts +22 -0
  90. package/src/app/[variants]/oauth/consent/[uid]/failed/page.tsx +0 -36
  91. package/src/app/[variants]/oauth/handoff/Client.tsx +0 -98
  92. package/src/app/[variants]/oauth/handoff/page.tsx +0 -13
@@ -101,7 +101,10 @@
101
101
  "waitingOAuth": {
102
102
  "cancel": "取消",
103
103
  "description": "瀏覽器已打開授權頁面,請在瀏覽器中完成授權",
104
+ "error": "授權失敗: {{error}}",
105
+ "errorTitle": "授權連接失敗",
104
106
  "helpText": "如果瀏覽器沒有自動打開,請點擊取消後重新嘗試",
107
+ "retry": "重試",
105
108
  "title": "等待授權連接"
106
109
  }
107
110
  }
@@ -28,10 +28,14 @@
28
28
  },
29
29
  "title": "授權 {{clientName}}"
30
30
  },
31
- "failed": {
31
+ "error": {
32
32
  "backToHome": "返回首頁",
33
- "subTitle": "您已拒絕授權應用訪問您的 LobeChat 帳戶",
34
- "title": "授權被拒絕"
33
+ "desc": "OAuth 授權失敗,失敗原因:{{reason}}",
34
+ "reason": {
35
+ "internal_error": "服務端錯誤",
36
+ "invalid_request": "無效的請求參數"
37
+ },
38
+ "title": "授權失敗"
35
39
  },
36
40
  "handoff": {
37
41
  "desc": {
@@ -50,7 +54,7 @@
50
54
  "userWelcome": "歡迎回來,"
51
55
  },
52
56
  "success": {
53
- "subTitle": "您已成功授權應用訪問您的 LobeChat 帳戶,可以關閉該頁面了",
57
+ "subTitle": "您已成功授權應用訪問您的帳戶,可以關閉該頁面了",
54
58
  "title": "授權成功"
55
59
  }
56
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.99.6",
3
+ "version": "1.100.0",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -218,7 +218,7 @@
218
218
  "numeral": "^2.0.6",
219
219
  "nuqs": "^2.4.3",
220
220
  "officeparser": "5.1.1",
221
- "oidc-provider": "^8.8.1",
221
+ "oidc-provider": "^9.2.0",
222
222
  "ollama": "^0.5.16",
223
223
  "openai": "^4.104.0",
224
224
  "openapi-fetch": "^0.9.8",
@@ -306,7 +306,7 @@
306
306
  "@types/lodash-es": "^4.17.12",
307
307
  "@types/node": "^22.15.29",
308
308
  "@types/numeral": "^2.0.5",
309
- "@types/oidc-provider": "^8.8.1",
309
+ "@types/oidc-provider": "^9.1.1",
310
310
  "@types/pg": "^8.15.4",
311
311
  "@types/react": "^19.1.6",
312
312
  "@types/react-dom": "^19.1.5",
@@ -1,7 +1,19 @@
1
- import { DispatchInvoke } from './types';
1
+ import { DispatchInvoke, type ProxyTRPCRequestParams } from './types';
2
+
3
+ interface StreamerCallbacks {
4
+ onData: (chunk: Uint8Array) => void;
5
+ onEnd: () => void;
6
+ onError: (error: Error) => void;
7
+ onResponse: (response: {
8
+ headers: Record<string, string>;
9
+ status: number;
10
+ statusText: string;
11
+ }) => void;
12
+ }
2
13
 
3
14
  interface IElectronAPI {
4
15
  invoke: DispatchInvoke;
16
+ onStreamInvoke: (params: ProxyTRPCRequestParams, callbacks: StreamerCallbacks) => () => void;
5
17
  }
6
18
 
7
19
  declare global {
@@ -11,7 +23,7 @@ declare global {
11
23
  }
12
24
 
13
25
  /**
14
- * client 端请求 sketch 端 event 数据的方法
26
+ * client 端请求 main 端 event 数据的方法
15
27
  */
16
28
  export const dispatch: DispatchInvoke = async (event, ...data) => {
17
29
  if (!window.electronAPI || !window.electronAPI.invoke)
@@ -1,4 +1,5 @@
1
1
  export * from './dispatch';
2
2
  export * from './events';
3
+ export * from './streamInvoke';
3
4
  export * from './types';
4
5
  export * from './useWatchBroadcast';
@@ -0,0 +1,62 @@
1
+ import { ProxyTRPCRequestParams } from './types';
2
+ import { headersToRecord } from './utils/headers';
3
+ import { getRequestBody } from './utils/request';
4
+
5
+ // eslint-disable-next-line no-undef
6
+ export const streamInvoke = async (input: RequestInfo | URL, init?: RequestInit) => {
7
+ const url = input.toString();
8
+ const parsedUrl = new URL(url, window.location.origin);
9
+ const urlPath = parsedUrl.pathname + parsedUrl.search;
10
+ const method = init?.method?.toUpperCase() || 'GET';
11
+ const headers = headersToRecord(init?.headers);
12
+ const body = await getRequestBody(init?.body);
13
+
14
+ const params: ProxyTRPCRequestParams = {
15
+ body,
16
+ headers,
17
+ method,
18
+ urlPath,
19
+ };
20
+
21
+ return new Promise<Response>((resolve, reject) => {
22
+ let streamController: ReadableStreamDefaultController<any>;
23
+ let responseResolved = false;
24
+
25
+ const stream = new ReadableStream({
26
+ cancel() {
27
+ // This will be called if the consumer of the stream calls .cancel()
28
+ // We should clean up the IPC listeners
29
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
30
+ cleanup?.();
31
+ },
32
+ start(controller) {
33
+ streamController = controller;
34
+ },
35
+ });
36
+
37
+ const cleanup = window.electronAPI.onStreamInvoke(params, {
38
+ onData: (chunk) => {
39
+ if (streamController) streamController.enqueue(chunk);
40
+ },
41
+ onEnd: () => {
42
+ if (streamController) streamController.close();
43
+ },
44
+ onError: (error) => {
45
+ console.error('[streamInvoke] Error during IPC stream proxy call:', error);
46
+ if (!responseResolved) {
47
+ responseResolved = true;
48
+ reject(error); // Reject the main promise if response not yet sent
49
+ } else if (streamController) {
50
+ streamController.error(error); // Otherwise, propagate error through the stream
51
+ }
52
+ },
53
+ onResponse: (meta) => {
54
+ if (responseResolved) return;
55
+ responseResolved = true;
56
+
57
+ const response = new Response(stream, meta);
58
+ resolve(response);
59
+ },
60
+ });
61
+ });
62
+ };
@@ -9,6 +9,11 @@ export type ProxyTRPCRequestParams = {
9
9
  urlPath: string;
10
10
  };
11
11
 
12
+ export interface ProxyTRPCStreamRequestParams extends Omit<ProxyTRPCRequestParams, 'body'> {
13
+ body?: ArrayBuffer;
14
+ requestId: string;
15
+ }
16
+
12
17
  export interface ProxyTRPCRequestResult {
13
18
  /** Response body (likely as ArrayBuffer or string) */
14
19
  body: ArrayBuffer | string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 将 HeadersInit 转换为 Record<string, string>
3
+ * @param headersInit - Headers 初始化对象
4
+ * @returns 转换后的记录对象
5
+ */
6
+ // eslint-disable-next-line no-undef
7
+ export const headersToRecord = (headersInit?: HeadersInit): Record<string, string> => {
8
+ const record: Record<string, string> = {};
9
+ if (!headersInit) {
10
+ return record;
11
+ }
12
+ if (headersInit instanceof Headers) {
13
+ headersInit.forEach((value, key) => {
14
+ record[key] = value;
15
+ });
16
+ } else if (Array.isArray(headersInit)) {
17
+ headersInit.forEach(([key, value]) => {
18
+ record[key] = value;
19
+ });
20
+ } else {
21
+ Object.assign(record, headersInit);
22
+ }
23
+ delete record['host'];
24
+ delete record['connection'];
25
+ delete record['content-length'];
26
+ return record;
27
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 从请求体中获取数据
3
+ * @param body - 请求体
4
+ * @returns 转换后的请求体数据
5
+ */
6
+ export const getRequestBody = async (
7
+ // eslint-disable-next-line no-undef
8
+ body?: BodyInit | null,
9
+ ): Promise<string | ArrayBuffer | undefined> => {
10
+ if (!body) {
11
+ return undefined;
12
+ }
13
+ if (typeof body === 'string') {
14
+ return body;
15
+ }
16
+ if (body instanceof ArrayBuffer) {
17
+ return body;
18
+ }
19
+ if (ArrayBuffer.isView(body)) {
20
+ return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) as ArrayBuffer;
21
+ }
22
+ if (body instanceof Blob) {
23
+ return await body.arrayBuffer();
24
+ }
25
+
26
+ console.warn('不支持的 IPC 代理请求体类型:', typeof body);
27
+ throw new Error('不支持的 IPC 代理请求体类型');
28
+ };
@@ -0,0 +1,58 @@
1
+ import debug from 'debug';
2
+ import { NextRequest, NextResponse, after } from 'next/server';
3
+
4
+ import { OAuthHandoffModel } from '@/database/models/oauthHandoff';
5
+ import { serverDB } from '@/database/server';
6
+
7
+ const log = debug('lobe-oidc:callback:desktop');
8
+
9
+ export const GET = async (req: NextRequest) => {
10
+ try {
11
+ const searchParams = req.nextUrl.searchParams;
12
+ const code = searchParams.get('code');
13
+ const state = searchParams.get('state'); // This `state` is the handoff ID
14
+
15
+ if (!code || !state || typeof code !== 'string' || typeof state !== 'string') {
16
+ log('Missing code or state in form data');
17
+ const errorUrl = req.nextUrl.clone();
18
+ errorUrl.pathname = '/oauth/callback/error';
19
+ errorUrl.searchParams.set('reason', 'invalid_request');
20
+ return NextResponse.redirect(errorUrl);
21
+ }
22
+
23
+ log('Received OIDC callback. state(handoffId): %s', state);
24
+
25
+ // The 'client' is 'desktop' because this redirect_uri is for the desktop client.
26
+ const client = 'desktop';
27
+ const payload = { code, state };
28
+ const id = state;
29
+
30
+ const authHandoffModel = new OAuthHandoffModel(serverDB);
31
+ await authHandoffModel.create({ client, id, payload });
32
+ log('Handoff record created successfully for id: %s', id);
33
+
34
+ // Redirect to a generic success page. The desktop app will poll for the result.
35
+ const successUrl = req.nextUrl.clone();
36
+ successUrl.pathname = '/oauth/callback/success';
37
+
38
+ // cleanup expired
39
+ after(async () => {
40
+ const cleanedCount = await authHandoffModel.cleanupExpired();
41
+
42
+ log('Cleaned up %d expired handoff records', cleanedCount);
43
+ });
44
+
45
+ return NextResponse.redirect(successUrl);
46
+ } catch (error) {
47
+ log('Error in OIDC callback: %O', error);
48
+ const errorUrl = req.nextUrl.clone();
49
+ errorUrl.pathname = '/oauth/callback/error';
50
+ errorUrl.searchParams.set('reason', 'internal_error');
51
+
52
+ if (error instanceof Error) {
53
+ errorUrl.searchParams.set('errorMessage', error.message);
54
+ }
55
+
56
+ return NextResponse.redirect(errorUrl);
57
+ }
58
+ };
@@ -0,0 +1,46 @@
1
+ import debug from 'debug';
2
+ import { NextRequest, NextResponse } from 'next/server';
3
+
4
+ import { OAuthHandoffModel } from '@/database/models/oauthHandoff';
5
+ import { serverDB } from '@/database/server';
6
+
7
+ const log = debug('lobe-oidc:handoff');
8
+
9
+ /**
10
+ * GET /oidc/handoff?id=xxx&client=xxx
11
+ * 轮询获取并消费认证凭证
12
+ */
13
+ export async function GET(request: NextRequest) {
14
+ log('Received GET request for /oidc/handoff');
15
+
16
+ try {
17
+ const { searchParams } = new URL(request.url);
18
+ const id = searchParams.get('id');
19
+ const client = searchParams.get('client');
20
+
21
+ if (!id || !client) {
22
+ return NextResponse.json(
23
+ { error: 'Missing required parameters: id and client' },
24
+ { status: 400 },
25
+ );
26
+ }
27
+
28
+ log('Fetching handoff record - id=%s, client=%s', id, client);
29
+
30
+ const authHandoffModel = new OAuthHandoffModel(serverDB);
31
+ const result = await authHandoffModel.fetchAndConsume(id, client);
32
+
33
+ if (!result) {
34
+ log('Handoff record not found or expired - id=%s', id);
35
+ return NextResponse.json({ error: 'Handoff record not found or expired' }, { status: 404 });
36
+ }
37
+
38
+ log('Handoff record found and consumed - id=%s', id);
39
+
40
+ return NextResponse.json({ data: result, success: true });
41
+ } catch (error) {
42
+ log('Error fetching handoff record: %O', error);
43
+
44
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
45
+ }
46
+ }
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { Button, Highlighter, Icon } from '@lobehub/ui';
4
+ import { Card, Result } from 'antd';
5
+ import { ShieldX } from 'lucide-react';
6
+ import Link from 'next/link';
7
+ import { parseAsString, useQueryState } from 'nuqs';
8
+ import React, { memo } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+ import { Center, Flexbox } from 'react-layout-kit';
11
+
12
+ const FailedPage = memo(() => {
13
+ const { t } = useTranslation('oauth');
14
+ const [reason] = useQueryState('reason');
15
+ const [errorMessage] = useQueryState<string>('errorMessage', parseAsString);
16
+
17
+ return (
18
+ <Center height="100vh">
19
+ <Card
20
+ style={{
21
+ alignItems: 'center',
22
+ display: 'flex',
23
+ justifyContent: 'center',
24
+ minHeight: 280,
25
+ minWidth: 500,
26
+ width: '100%',
27
+ }}
28
+ >
29
+ <Result
30
+ extra={
31
+ <Link href="/">
32
+ <Button type="primary">{t('error.backToHome')}</Button>
33
+ </Link>
34
+ }
35
+ icon={<Icon icon={ShieldX} />}
36
+ status="error"
37
+ subTitle={
38
+ <Flexbox gap={8}>
39
+ {t('error.desc', {
40
+ reason: t(`error.reason.${reason}` as any, { defaultValue: reason }),
41
+ })}
42
+
43
+ {!!errorMessage && <Highlighter language={'log'}>{errorMessage}</Highlighter>}
44
+ </Flexbox>
45
+ }
46
+ title={t('error.title')}
47
+ />
48
+ </Card>
49
+ </Center>
50
+ );
51
+ });
52
+
53
+ FailedPage.displayName = 'FailedPage';
54
+
55
+ export default FailedPage;
@@ -0,0 +1,12 @@
1
+ import { notFound } from 'next/navigation';
2
+ import { PropsWithChildren } from 'react';
3
+
4
+ import { oidcEnv } from '@/envs/oidc';
5
+
6
+ const Layout = ({ children }: PropsWithChildren) => {
7
+ if (!oidcEnv.ENABLE_OIDC) return notFound();
8
+
9
+ return children;
10
+ };
11
+
12
+ export default Layout;
@@ -0,0 +1,3 @@
1
+ 'use client';
2
+
3
+ export { default } from '@/components/Loading/BrandTextLoading';
@@ -12,7 +12,16 @@ const SuccessPage = memo(() => {
12
12
 
13
13
  return (
14
14
  <Center height="100vh">
15
- <Card style={{ maxWidth: 500, width: '100%' }}>
15
+ <Card
16
+ style={{
17
+ alignItems: 'center',
18
+ display: 'flex',
19
+ justifyContent: 'center',
20
+ minHeight: 280,
21
+ minWidth: 500,
22
+ width: '100%',
23
+ }}
24
+ >
16
25
  <Result
17
26
  icon={<Icon icon={CheckCircle} />}
18
27
  status="success"
@@ -3,7 +3,7 @@
3
3
  import { Button, Text } from '@lobehub/ui';
4
4
  import { Card, Divider } from 'antd';
5
5
  import { createStyles } from 'antd-style';
6
- import { memo } from 'react';
6
+ import { memo, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Center, Flexbox } from 'react-layout-kit';
9
9
 
@@ -122,6 +122,8 @@ const ConsentClient = memo<ClientProps>(
122
122
  const { styles, theme } = useStyles();
123
123
  const { t } = useTranslation('oauth');
124
124
 
125
+ const [isLoading, setIsLoading] = useState(false);
126
+
125
127
  const clientDisplayName = clientMetadata?.clientName || clientId;
126
128
  return (
127
129
  <Center className={styles.container} gap={16}>
@@ -165,7 +167,11 @@ const ConsentClient = memo<ClientProps>(
165
167
  <Button
166
168
  className={styles.authButton}
167
169
  htmlType="submit"
170
+ loading={isLoading}
168
171
  name="consent"
172
+ onClick={() => {
173
+ setIsLoading(true);
174
+ }}
169
175
  type="primary"
170
176
  value="accept"
171
177
  >
@@ -541,5 +541,13 @@
541
541
  "bps": true,
542
542
  "folderMillis": 1752413805765,
543
543
  "hash": "abed92b1356df6d7eb35c03f47fbbdcdaf25aefda750dc3e4963c1c2a0d38b54"
544
+ },
545
+ {
546
+ "sql": [
547
+ "CREATE TABLE \"oauth_handoffs\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"client\" varchar(50) NOT NULL,\n\t\"payload\" jsonb NOT NULL,\n\t\"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n"
548
+ ],
549
+ "bps": true,
550
+ "folderMillis": 1752567402506,
551
+ "hash": "8ba3ae52ed72e8aad1623dbcf47ca26a8406ebffc6d5284abff94ea994b59c04"
544
552
  }
545
553
  ]
@@ -0,0 +1,8 @@
1
+ CREATE TABLE "oauth_handoffs" (
2
+ "id" text PRIMARY KEY NOT NULL,
3
+ "client" varchar(50) NOT NULL,
4
+ "payload" jsonb NOT NULL,
5
+ "accessed_at" timestamp with time zone DEFAULT now() NOT NULL,
6
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
7
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
8
+ );