@basictech/react 0.8.0-beta.2 → 0.8.0-beta.4
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 +12 -0
- package/dist/index.d.mts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +591 -147
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +599 -148
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/readme.md +99 -97
- package/src/AuthContext.tsx +513 -411
- package/src/context.tsx +19 -1
- package/src/core/auth/AuthManager.ts +1239 -726
- package/src/sync/syncProtocol.js +49 -3
- package/src/utils/network.ts +1 -1
package/src/AuthContext.tsx
CHANGED
|
@@ -1,489 +1,591 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
Suspense,
|
|
7
|
+
lazy,
|
|
8
|
+
} from 'react'
|
|
2
9
|
|
|
3
10
|
import { BasicSync, initDexieExtensions } from './sync'
|
|
4
11
|
import { RemoteDB, DBMode, BasicDB } from './core/db'
|
|
5
12
|
import { AuthManager } from './core/auth/AuthManager'
|
|
6
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
User,
|
|
15
|
+
AuthResult,
|
|
16
|
+
GetTokenOptions,
|
|
17
|
+
PdsEndpoints,
|
|
18
|
+
AuthStatus,
|
|
19
|
+
} from './core/auth/AuthManager'
|
|
7
20
|
|
|
8
21
|
import { log } from './config'
|
|
9
22
|
import { version as currentVersion } from '../package.json'
|
|
10
23
|
import { createVersionUpdater } from './updater/versionUpdater'
|
|
11
24
|
import { getMigrations } from './updater/updateMigrations'
|
|
12
25
|
import { BasicStorage, LocalStorageAdapter } from './utils/storage'
|
|
13
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
isDevelopment,
|
|
28
|
+
checkForNewVersion,
|
|
29
|
+
getSyncStatus,
|
|
30
|
+
} from './utils/network'
|
|
14
31
|
import { validateAndCheckSchema } from './utils/schema'
|
|
15
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
BasicContext,
|
|
34
|
+
DBStatus,
|
|
35
|
+
noDb,
|
|
36
|
+
type BasicSchemaDevInfo,
|
|
37
|
+
} from './context'
|
|
16
38
|
|
|
17
39
|
const BasicDevToolbar = lazy(() =>
|
|
18
|
-
|
|
40
|
+
import('./dev/BasicDevToolbar').then((m) => ({ default: m.BasicDevToolbar })),
|
|
19
41
|
)
|
|
20
42
|
|
|
21
43
|
export type { BasicStorage, LocalStorageAdapter } from './utils/storage'
|
|
22
44
|
export type { DBMode, BasicDB, Collection } from './core/db'
|
|
23
|
-
export type {
|
|
45
|
+
export type {
|
|
46
|
+
Token,
|
|
47
|
+
User,
|
|
48
|
+
AuthResult,
|
|
49
|
+
GetTokenOptions,
|
|
50
|
+
PdsEndpoints,
|
|
51
|
+
AuthStatus,
|
|
52
|
+
} from './core/auth/AuthManager'
|
|
24
53
|
export type { BasicContextType, BasicSchemaDevInfo } from './context'
|
|
25
54
|
export { DBStatus, useBasic, BasicContext } from './context'
|
|
26
55
|
|
|
27
56
|
export type AuthConfig = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
scopes?: string | string[]
|
|
58
|
+
/** @deprecated Use pds_url instead */
|
|
59
|
+
server_url?: string
|
|
60
|
+
/** PDS URL for auth and data (default: https://pds.basic.id) */
|
|
61
|
+
pds_url?: string
|
|
62
|
+
/** Admin server URL for connect reporting (default: https://api.basic.tech) */
|
|
63
|
+
admin_url?: string
|
|
64
|
+
ws_url?: string
|
|
36
65
|
}
|
|
37
66
|
|
|
38
67
|
export type BasicProviderProps = {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
children: React.ReactNode
|
|
69
|
+
/**
|
|
70
|
+
* @deprecated Project ID is now extracted from schema.project_id.
|
|
71
|
+
* This prop is kept for backward compatibility but can be omitted.
|
|
72
|
+
*/
|
|
73
|
+
project_id?: string
|
|
74
|
+
/** The Basic schema object containing project_id and table definitions */
|
|
75
|
+
schema?: any
|
|
76
|
+
debug?: boolean
|
|
77
|
+
storage?: BasicStorage
|
|
78
|
+
auth?: AuthConfig
|
|
79
|
+
/**
|
|
80
|
+
* Database mode - determines which implementation is used
|
|
81
|
+
* - 'sync': Uses Dexie + WebSocket for local-first sync (default)
|
|
82
|
+
* - 'remote': Uses REST API calls directly to server
|
|
83
|
+
*/
|
|
84
|
+
dbMode?: DBMode
|
|
85
|
+
/** Show floating dev toolbar (localhost, NODE_ENV=development, or debug=true). */
|
|
86
|
+
devToolbar?: boolean
|
|
58
87
|
}
|
|
59
88
|
|
|
60
89
|
const DEFAULT_AUTH_CONFIG = {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
scopes: 'profile,email,app:admin',
|
|
91
|
+
pds_url: 'https://pds.basic.id',
|
|
92
|
+
admin_url: 'https://api.basic.tech',
|
|
93
|
+
ws_url: 'wss://pds.basic.id/ws',
|
|
65
94
|
} as const
|
|
66
95
|
|
|
67
96
|
type ErrorObject = {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
97
|
+
code: string
|
|
98
|
+
title: string
|
|
99
|
+
message: string
|
|
71
100
|
}
|
|
72
101
|
|
|
73
102
|
type AuthSnapshot = {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
103
|
+
isSignedIn: boolean
|
|
104
|
+
hasToken: boolean
|
|
105
|
+
isAuthReady: boolean
|
|
106
|
+
authStatus: AuthStatus
|
|
107
|
+
authErrorCode: string | null
|
|
108
|
+
user: User | null
|
|
109
|
+
did: string | null
|
|
110
|
+
tokenScope: string | null
|
|
80
111
|
}
|
|
81
112
|
|
|
82
113
|
function snapshotAuth(mgr: AuthManager): AuthSnapshot {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
114
|
+
return {
|
|
115
|
+
isSignedIn: mgr.isSignedIn,
|
|
116
|
+
hasToken: !!mgr.token,
|
|
117
|
+
isAuthReady: mgr.isAuthReady,
|
|
118
|
+
authStatus: mgr.authStatus,
|
|
119
|
+
authErrorCode: mgr.authErrorCode,
|
|
120
|
+
user: mgr.user,
|
|
121
|
+
did: mgr.did,
|
|
122
|
+
tokenScope: mgr.tokenScope,
|
|
123
|
+
}
|
|
91
124
|
}
|
|
92
125
|
|
|
93
126
|
export function BasicProvider({
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
127
|
+
children,
|
|
128
|
+
project_id: project_id_prop,
|
|
129
|
+
schema,
|
|
130
|
+
debug = false,
|
|
131
|
+
storage,
|
|
132
|
+
auth,
|
|
133
|
+
dbMode = 'sync',
|
|
134
|
+
devToolbar = false,
|
|
102
135
|
}: BasicProviderProps) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
136
|
+
const project_id = schema?.project_id || project_id_prop
|
|
137
|
+
|
|
138
|
+
if (auth?.server_url && !auth?.pds_url) {
|
|
139
|
+
log('Warning: auth.server_url is deprecated, use auth.pds_url instead')
|
|
140
|
+
}
|
|
141
|
+
const authConfig = {
|
|
142
|
+
scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
|
|
143
|
+
pds_url: auth?.pds_url || auth?.server_url || DEFAULT_AUTH_CONFIG.pds_url,
|
|
144
|
+
admin_url: auth?.admin_url || DEFAULT_AUTH_CONFIG.admin_url,
|
|
145
|
+
ws_url: auth?.ws_url || DEFAULT_AUTH_CONFIG.ws_url,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const scopesString = Array.isArray(authConfig.scopes)
|
|
149
|
+
? authConfig.scopes.join(' ')
|
|
150
|
+
: authConfig.scopes
|
|
151
|
+
|
|
152
|
+
const storageRef = useRef<BasicStorage>(storage || new LocalStorageAdapter())
|
|
153
|
+
const storageAdapter = storageRef.current
|
|
154
|
+
|
|
155
|
+
const schemaRef = useRef(schema)
|
|
156
|
+
schemaRef.current = schema
|
|
157
|
+
|
|
158
|
+
const [authState, setAuthState] = useState<AuthSnapshot>({
|
|
159
|
+
isSignedIn: false,
|
|
160
|
+
hasToken: false,
|
|
161
|
+
isAuthReady: false,
|
|
162
|
+
authStatus: 'bootstrapping',
|
|
163
|
+
authErrorCode: null,
|
|
164
|
+
user: null,
|
|
165
|
+
did: null,
|
|
166
|
+
tokenScope: null,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const authRef = useRef<AuthManager>(null!)
|
|
170
|
+
if (!authRef.current) {
|
|
171
|
+
authRef.current = new AuthManager(
|
|
172
|
+
{
|
|
173
|
+
projectId: project_id,
|
|
174
|
+
scopes: scopesString,
|
|
175
|
+
pdsUrl: authConfig.pds_url,
|
|
176
|
+
adminUrl: authConfig.admin_url,
|
|
177
|
+
debug,
|
|
178
|
+
},
|
|
179
|
+
storageAdapter,
|
|
180
|
+
() => setAuthState(snapshotAuth(authRef.current)),
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const syncRef = useRef<BasicSync | null>(null)
|
|
185
|
+
const remoteDbRef = useRef<RemoteDB | null>(null)
|
|
186
|
+
const [shouldConnect, setShouldConnect] = useState(false)
|
|
187
|
+
const [dbStatus, setDbStatus] = useState<DBStatus>(DBStatus.OFFLINE)
|
|
188
|
+
const [isDbReady, setIsDbReady] = useState(false)
|
|
189
|
+
const [error, setError] = useState<ErrorObject | null>(null)
|
|
190
|
+
const [schemaDevInfo, setSchemaDevInfo] = useState<BasicSchemaDevInfo | null>(
|
|
191
|
+
null,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const isDevMode = () => isDevelopment(debug)
|
|
195
|
+
|
|
196
|
+
const refreshSchemaStatus = useCallback(async () => {
|
|
197
|
+
const s = schemaRef.current
|
|
198
|
+
if (!s) {
|
|
199
|
+
setSchemaDevInfo(
|
|
200
|
+
project_id
|
|
201
|
+
? {
|
|
202
|
+
projectId: project_id,
|
|
203
|
+
localVersion: undefined,
|
|
204
|
+
status: 'no_schema',
|
|
205
|
+
valid: false,
|
|
206
|
+
lastCheckedAt: Date.now(),
|
|
207
|
+
}
|
|
208
|
+
: null,
|
|
209
|
+
)
|
|
210
|
+
return
|
|
107
211
|
}
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
212
|
+
const result = await validateAndCheckSchema(s)
|
|
213
|
+
if (!result.isValid) {
|
|
214
|
+
const errText =
|
|
215
|
+
result.errors
|
|
216
|
+
?.map((e: { message?: string }) => e.message || '')
|
|
217
|
+
.join('; ') || 'invalid'
|
|
218
|
+
setSchemaDevInfo({
|
|
219
|
+
projectId: s.project_id ?? null,
|
|
220
|
+
localVersion: s.version,
|
|
221
|
+
status: 'invalid',
|
|
222
|
+
valid: false,
|
|
223
|
+
lastCheckedAt: Date.now(),
|
|
224
|
+
error: errText,
|
|
225
|
+
})
|
|
226
|
+
return
|
|
113
227
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const storageAdapter = storageRef.current
|
|
121
|
-
|
|
122
|
-
const schemaRef = useRef(schema)
|
|
123
|
-
schemaRef.current = schema
|
|
124
|
-
|
|
125
|
-
const [authState, setAuthState] = useState<AuthSnapshot>({
|
|
126
|
-
isSignedIn: false,
|
|
127
|
-
hasToken: false,
|
|
128
|
-
isAuthReady: false,
|
|
129
|
-
user: null,
|
|
130
|
-
did: null,
|
|
131
|
-
tokenScope: null,
|
|
228
|
+
setSchemaDevInfo({
|
|
229
|
+
projectId: s.project_id ?? null,
|
|
230
|
+
localVersion: s.version,
|
|
231
|
+
status: result.schemaStatus.status ?? 'unknown',
|
|
232
|
+
valid: result.schemaStatus.valid,
|
|
233
|
+
lastCheckedAt: Date.now(),
|
|
132
234
|
})
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
debug,
|
|
143
|
-
},
|
|
144
|
-
storageAdapter,
|
|
145
|
-
() => setAuthState(snapshotAuth(authRef.current)),
|
|
235
|
+
}, [project_id])
|
|
236
|
+
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
const runVersionUpdater = async () => {
|
|
239
|
+
try {
|
|
240
|
+
const versionUpdater = createVersionUpdater(
|
|
241
|
+
storageAdapter,
|
|
242
|
+
currentVersion,
|
|
243
|
+
getMigrations(),
|
|
146
244
|
)
|
|
147
|
-
|
|
245
|
+
const updateResult = await versionUpdater.checkAndUpdate()
|
|
148
246
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const [schemaDevInfo, setSchemaDevInfo] = useState<BasicSchemaDevInfo | null>(null)
|
|
156
|
-
|
|
157
|
-
const isDevMode = () => isDevelopment(debug)
|
|
158
|
-
|
|
159
|
-
const refreshSchemaStatus = useCallback(async () => {
|
|
160
|
-
const s = schemaRef.current
|
|
161
|
-
if (!s) {
|
|
162
|
-
setSchemaDevInfo(
|
|
163
|
-
project_id
|
|
164
|
-
? {
|
|
165
|
-
projectId: project_id,
|
|
166
|
-
localVersion: undefined,
|
|
167
|
-
status: 'no_schema',
|
|
168
|
-
valid: false,
|
|
169
|
-
lastCheckedAt: Date.now(),
|
|
170
|
-
}
|
|
171
|
-
: null,
|
|
172
|
-
)
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
const result = await validateAndCheckSchema(s)
|
|
176
|
-
if (!result.isValid) {
|
|
177
|
-
const errText =
|
|
178
|
-
result.errors?.map((e: { message?: string }) => e.message || '').join('; ') || 'invalid'
|
|
179
|
-
setSchemaDevInfo({
|
|
180
|
-
projectId: s.project_id ?? null,
|
|
181
|
-
localVersion: s.version,
|
|
182
|
-
status: 'invalid',
|
|
183
|
-
valid: false,
|
|
184
|
-
lastCheckedAt: Date.now(),
|
|
185
|
-
error: errText,
|
|
186
|
-
})
|
|
187
|
-
return
|
|
188
|
-
}
|
|
189
|
-
setSchemaDevInfo({
|
|
190
|
-
projectId: s.project_id ?? null,
|
|
191
|
-
localVersion: s.version,
|
|
192
|
-
status: result.schemaStatus.status ?? 'unknown',
|
|
193
|
-
valid: result.schemaStatus.valid,
|
|
194
|
-
lastCheckedAt: Date.now(),
|
|
195
|
-
})
|
|
196
|
-
}, [project_id])
|
|
197
|
-
|
|
198
|
-
useEffect(() => {
|
|
199
|
-
const runVersionUpdater = async () => {
|
|
200
|
-
try {
|
|
201
|
-
const versionUpdater = createVersionUpdater(storageAdapter, currentVersion, getMigrations())
|
|
202
|
-
const updateResult = await versionUpdater.checkAndUpdate()
|
|
203
|
-
|
|
204
|
-
if (updateResult.updated) {
|
|
205
|
-
log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`)
|
|
206
|
-
} else {
|
|
207
|
-
log(`App version ${updateResult.toVersion} is current`)
|
|
208
|
-
}
|
|
209
|
-
} catch (error) {
|
|
210
|
-
log('Version update failed:', error)
|
|
211
|
-
}
|
|
247
|
+
if (updateResult.updated) {
|
|
248
|
+
log(
|
|
249
|
+
`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`,
|
|
250
|
+
)
|
|
251
|
+
} else {
|
|
252
|
+
log(`App version ${updateResult.toVersion} is current`)
|
|
212
253
|
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
log('Version update failed:', error)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
213
258
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return authRef.current.setupNetworkListeners()
|
|
218
|
-
}, [])
|
|
259
|
+
runVersionUpdater()
|
|
260
|
+
authRef.current.initialize()
|
|
219
261
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (!syncRef.current) {
|
|
223
|
-
log('Initializing Basic Sync DB')
|
|
262
|
+
return authRef.current.setupNetworkListeners()
|
|
263
|
+
}, [])
|
|
224
264
|
|
|
225
|
-
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
async function initSyncDb(options: { shouldConnect: boolean }) {
|
|
267
|
+
if (!syncRef.current) {
|
|
268
|
+
log('Initializing Basic Sync DB')
|
|
226
269
|
|
|
227
|
-
|
|
270
|
+
await initDexieExtensions()
|
|
228
271
|
|
|
229
|
-
|
|
230
|
-
const newStatus = getSyncStatus(status) as DBStatus
|
|
231
|
-
setDbStatus(newStatus)
|
|
272
|
+
syncRef.current = new BasicSync('basicdb', { schema: schema })
|
|
232
273
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
})
|
|
274
|
+
syncRef.current.syncable.on('statusChanged', (status: number) => {
|
|
275
|
+
const newStatus = getSyncStatus(status) as DBStatus
|
|
276
|
+
setDbStatus(newStatus)
|
|
238
277
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
278
|
+
if (newStatus === DBStatus.ERROR_WILL_RETRY) {
|
|
279
|
+
log(
|
|
280
|
+
'Sync entered ERROR_WILL_RETRY - reconciling auth session before retry',
|
|
281
|
+
)
|
|
282
|
+
authRef.current
|
|
283
|
+
.reconcileSession('sync retry', {
|
|
284
|
+
forceRefresh: true,
|
|
285
|
+
throttleMs: 0,
|
|
286
|
+
})
|
|
287
|
+
.catch(() => {})
|
|
288
|
+
}
|
|
289
|
+
})
|
|
244
290
|
|
|
245
|
-
|
|
246
|
-
|
|
291
|
+
if (options.shouldConnect) {
|
|
292
|
+
setShouldConnect(true)
|
|
293
|
+
} else {
|
|
294
|
+
log('Sync is disabled')
|
|
247
295
|
}
|
|
248
296
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
serverUrl: authConfig.pds_url,
|
|
265
|
-
projectId: project_id,
|
|
266
|
-
getToken: (opts) => authRef.current.getToken(opts),
|
|
267
|
-
schema: schema,
|
|
268
|
-
debug: debug,
|
|
269
|
-
onAuthError: (error) => {
|
|
270
|
-
log('RemoteDB auth error:', error)
|
|
271
|
-
handleSignOut()
|
|
272
|
-
},
|
|
273
|
-
})
|
|
274
|
-
setDbStatus(DBStatus.ONLINE)
|
|
275
|
-
setIsReady(true)
|
|
276
|
-
}
|
|
297
|
+
setIsDbReady(true)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function initRemoteDb() {
|
|
302
|
+
if (!remoteDbRef.current) {
|
|
303
|
+
if (!project_id) {
|
|
304
|
+
setError({
|
|
305
|
+
code: 'missing_project_id',
|
|
306
|
+
title: 'Project ID Required',
|
|
307
|
+
message:
|
|
308
|
+
'Remote mode requires a project_id. Provide it via schema.project_id or the project_id prop.',
|
|
309
|
+
})
|
|
310
|
+
setIsDbReady(true)
|
|
311
|
+
return
|
|
277
312
|
}
|
|
278
313
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
localVersion: schema?.version,
|
|
292
|
-
status: 'invalid',
|
|
293
|
-
valid: false,
|
|
294
|
-
lastCheckedAt: Date.now(),
|
|
295
|
-
error: errorMessage.trim() || undefined,
|
|
296
|
-
})
|
|
297
|
-
setError({
|
|
298
|
-
code: 'schema_invalid',
|
|
299
|
-
title: 'Basic Schema is invalid!',
|
|
300
|
-
message: errorMessage,
|
|
301
|
-
})
|
|
302
|
-
setIsReady(true)
|
|
303
|
-
return null
|
|
314
|
+
log('Initializing Basic Remote DB')
|
|
315
|
+
remoteDbRef.current = new RemoteDB({
|
|
316
|
+
serverUrl: authConfig.pds_url,
|
|
317
|
+
projectId: project_id,
|
|
318
|
+
getToken: (opts) => authRef.current.getToken(opts),
|
|
319
|
+
schema: schema,
|
|
320
|
+
debug: debug,
|
|
321
|
+
onAuthError: (error) => {
|
|
322
|
+
log('RemoteDB auth error:', error)
|
|
323
|
+
if (error.errorType === 'forbidden') {
|
|
324
|
+
log('403 Forbidden - user lacks required scope, not signing out')
|
|
325
|
+
return
|
|
304
326
|
}
|
|
327
|
+
authRef.current
|
|
328
|
+
.reconcileSession(`remote db ${error.errorType}`, {
|
|
329
|
+
forceRefresh: error.errorType !== 'network',
|
|
330
|
+
throttleMs: 0,
|
|
331
|
+
})
|
|
332
|
+
.catch((reconcileError) => {
|
|
333
|
+
log('RemoteDB auth recovery failed:', reconcileError)
|
|
334
|
+
})
|
|
335
|
+
},
|
|
336
|
+
})
|
|
337
|
+
setDbStatus(DBStatus.ONLINE)
|
|
338
|
+
setIsDbReady(true)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
305
341
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
localVersion: schema?.version,
|
|
309
|
-
status: result.schemaStatus.status ?? 'unknown',
|
|
310
|
-
valid: result.schemaStatus.valid,
|
|
311
|
-
lastCheckedAt: Date.now(),
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
if (dbMode === 'remote') {
|
|
315
|
-
initRemoteDb()
|
|
316
|
-
} else {
|
|
317
|
-
if (result.schemaStatus.valid) {
|
|
318
|
-
await initSyncDb({ shouldConnect: true })
|
|
319
|
-
} else {
|
|
320
|
-
if (result.schemaStatus.status === 'unpublished') {
|
|
321
|
-
log('Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync.')
|
|
322
|
-
} else {
|
|
323
|
-
log('Schema is invalid!', result.schemaStatus)
|
|
324
|
-
}
|
|
325
|
-
await initSyncDb({ shouldConnect: false })
|
|
326
|
-
}
|
|
327
|
-
}
|
|
342
|
+
async function checkSchema() {
|
|
343
|
+
const result = await validateAndCheckSchema(schema)
|
|
328
344
|
|
|
329
|
-
|
|
345
|
+
if (!result.isValid) {
|
|
346
|
+
let errorMessage = ''
|
|
347
|
+
if (result.errors) {
|
|
348
|
+
result.errors.forEach((err: any, index: number) => {
|
|
349
|
+
errorMessage += `${index + 1}: ${err.message} - at ${err.instancePath}\n`
|
|
350
|
+
})
|
|
330
351
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
352
|
+
setSchemaDevInfo({
|
|
353
|
+
projectId: schema?.project_id ?? null,
|
|
354
|
+
localVersion: schema?.version,
|
|
355
|
+
status: 'invalid',
|
|
356
|
+
valid: false,
|
|
357
|
+
lastCheckedAt: Date.now(),
|
|
358
|
+
error: errorMessage.trim() || undefined,
|
|
359
|
+
})
|
|
360
|
+
setError({
|
|
361
|
+
code: 'schema_invalid',
|
|
362
|
+
title: 'Basic Schema is invalid!',
|
|
363
|
+
message: errorMessage,
|
|
364
|
+
})
|
|
365
|
+
setIsDbReady(true)
|
|
366
|
+
return null
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
setSchemaDevInfo({
|
|
370
|
+
projectId: schema?.project_id ?? null,
|
|
371
|
+
localVersion: schema?.version,
|
|
372
|
+
status: result.schemaStatus.status ?? 'unknown',
|
|
373
|
+
valid: result.schemaStatus.valid,
|
|
374
|
+
lastCheckedAt: Date.now(),
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
if (dbMode === 'remote') {
|
|
378
|
+
initRemoteDb()
|
|
379
|
+
} else {
|
|
380
|
+
if (result.schemaStatus.valid) {
|
|
381
|
+
await initSyncDb({ shouldConnect: true })
|
|
334
382
|
} else {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
projectId: project_id,
|
|
339
|
-
localVersion: undefined,
|
|
340
|
-
status: 'no_schema',
|
|
341
|
-
valid: false,
|
|
342
|
-
lastCheckedAt: Date.now(),
|
|
343
|
-
}
|
|
344
|
-
: null,
|
|
383
|
+
if (result.schemaStatus.status === 'unpublished') {
|
|
384
|
+
log(
|
|
385
|
+
'Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync.',
|
|
345
386
|
)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
387
|
+
} else {
|
|
388
|
+
log('Schema is invalid!', result.schemaStatus)
|
|
389
|
+
}
|
|
390
|
+
await initSyncDb({ shouldConnect: false })
|
|
351
391
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
useEffect(() => {
|
|
355
|
-
if (authState.hasToken && syncRef.current && authState.isSignedIn && shouldConnect) {
|
|
356
|
-
log('connecting to db...')
|
|
357
|
-
|
|
358
|
-
syncRef.current
|
|
359
|
-
?.connect({
|
|
360
|
-
getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
|
|
361
|
-
ws_url: authConfig.ws_url,
|
|
362
|
-
})
|
|
363
|
-
.catch((e: any) => {
|
|
364
|
-
log('error connecting to db', e)
|
|
365
|
-
})
|
|
366
|
-
}
|
|
367
|
-
}, [authState.isSignedIn, authState.hasToken, shouldConnect])
|
|
368
|
-
|
|
369
|
-
const handleSignOut = async () => {
|
|
370
|
-
await authRef.current.signOut()
|
|
371
|
-
if (syncRef.current) {
|
|
372
|
-
try {
|
|
373
|
-
await syncRef.current.close()
|
|
374
|
-
await syncRef.current.delete({ disableAutoOpen: false })
|
|
375
|
-
syncRef.current = null
|
|
376
|
-
window?.location?.reload()
|
|
377
|
-
} catch (error) {
|
|
378
|
-
console.error('Error during database cleanup:', error)
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
392
|
+
}
|
|
382
393
|
|
|
383
|
-
|
|
384
|
-
try {
|
|
385
|
-
await authRef.current.signIn()
|
|
386
|
-
} catch (error) {
|
|
387
|
-
if (isDevMode()) {
|
|
388
|
-
setError({
|
|
389
|
-
code: 'signin_error',
|
|
390
|
-
title: 'Sign-in Failed',
|
|
391
|
-
message:
|
|
392
|
-
(error as Error).message || 'An error occurred during sign-in. Please try again.',
|
|
393
|
-
})
|
|
394
|
-
}
|
|
395
|
-
throw error
|
|
396
|
-
}
|
|
394
|
+
checkForNewVersion()
|
|
397
395
|
}
|
|
398
396
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
397
|
+
if (schema) {
|
|
398
|
+
checkSchema()
|
|
399
|
+
} else {
|
|
400
|
+
setSchemaDevInfo(
|
|
401
|
+
project_id
|
|
402
|
+
? {
|
|
403
|
+
projectId: project_id,
|
|
404
|
+
localVersion: undefined,
|
|
405
|
+
status: 'no_schema',
|
|
406
|
+
valid: false,
|
|
407
|
+
lastCheckedAt: Date.now(),
|
|
410
408
|
}
|
|
411
|
-
|
|
412
|
-
|
|
409
|
+
: null,
|
|
410
|
+
)
|
|
411
|
+
if (dbMode === 'remote' && project_id) {
|
|
412
|
+
initRemoteDb()
|
|
413
|
+
} else {
|
|
414
|
+
setIsDbReady(true)
|
|
415
|
+
}
|
|
413
416
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
417
|
+
}, [])
|
|
418
|
+
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
if (
|
|
421
|
+
authState.hasToken &&
|
|
422
|
+
syncRef.current &&
|
|
423
|
+
authState.isSignedIn &&
|
|
424
|
+
authState.authStatus !== 'reauth_required' &&
|
|
425
|
+
shouldConnect
|
|
426
|
+
) {
|
|
427
|
+
log('connecting to db...')
|
|
428
|
+
|
|
429
|
+
syncRef.current
|
|
430
|
+
?.connect({
|
|
431
|
+
getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
|
|
432
|
+
ws_url: authConfig.ws_url,
|
|
433
|
+
})
|
|
434
|
+
.catch((e: any) => {
|
|
435
|
+
log('error connecting to db', e)
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
}, [
|
|
439
|
+
authState.authStatus,
|
|
440
|
+
authState.isSignedIn,
|
|
441
|
+
authState.hasToken,
|
|
442
|
+
shouldConnect,
|
|
443
|
+
])
|
|
444
|
+
|
|
445
|
+
useEffect(() => {
|
|
446
|
+
if (authState.authStatus !== 'reauth_required' || !syncRef.current) {
|
|
447
|
+
return
|
|
420
448
|
}
|
|
421
449
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
isAuthReady: authState.isAuthReady,
|
|
447
|
-
signin: handleSignIn,
|
|
448
|
-
signout: handleSignOut,
|
|
449
|
-
signinWithCode: (code: string, state?: string) => authRef.current.signInWithCode(code, state),
|
|
450
|
-
getSignInLink: (redirectUri?: string) => authRef.current.getSignInUrl(redirectUri),
|
|
450
|
+
log('Auth requires reauthentication - disconnecting sync without deleting local DB')
|
|
451
|
+
setDbStatus(DBStatus.ERROR_TOKEN_EXPIRED)
|
|
452
|
+
syncRef.current
|
|
453
|
+
.disconnect({ ws_url: authConfig.ws_url })
|
|
454
|
+
.catch((disconnectError: unknown) => {
|
|
455
|
+
log('Error disconnecting sync after auth invalidation:', disconnectError)
|
|
456
|
+
})
|
|
457
|
+
}, [authConfig.ws_url, authState.authStatus])
|
|
458
|
+
|
|
459
|
+
// TODO: replace reload with proper sync DB teardown + re-init so
|
|
460
|
+
// sign-out → sign-in works without a full page reload.
|
|
461
|
+
const handleSignOut = async () => {
|
|
462
|
+
await authRef.current.signOut()
|
|
463
|
+
if (syncRef.current) {
|
|
464
|
+
try {
|
|
465
|
+
await syncRef.current.close()
|
|
466
|
+
await syncRef.current.delete({ disableAutoOpen: false })
|
|
467
|
+
syncRef.current = null
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error('Error during database cleanup:', error)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (typeof window !== 'undefined') {
|
|
473
|
+
window.location.reload()
|
|
451
474
|
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const handleSignIn = async () => {
|
|
478
|
+
try {
|
|
479
|
+
await authRef.current.signIn()
|
|
480
|
+
} catch (error) {
|
|
481
|
+
if (isDevMode()) {
|
|
482
|
+
setError({
|
|
483
|
+
code: 'signin_error',
|
|
484
|
+
title: 'Sign-in Failed',
|
|
485
|
+
message:
|
|
486
|
+
(error as Error).message ||
|
|
487
|
+
'An error occurred during sign-in. Please try again.',
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
throw error
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const handleSignInWithHandle = async (handle: string) => {
|
|
495
|
+
try {
|
|
496
|
+
await authRef.current.signInWithHandle(handle)
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (isDevMode()) {
|
|
499
|
+
setError({
|
|
500
|
+
code: 'signin_error',
|
|
501
|
+
title: 'Sign-in Failed',
|
|
502
|
+
message:
|
|
503
|
+
(error as Error).message ||
|
|
504
|
+
'An error occurred during sign-in. Please try again.',
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
throw error
|
|
508
|
+
}
|
|
509
|
+
}
|
|
452
510
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
511
|
+
const getCurrentDb = (): BasicDB => {
|
|
512
|
+
if (dbMode === 'remote') {
|
|
513
|
+
return remoteDbRef.current || noDb
|
|
514
|
+
}
|
|
515
|
+
return syncRef.current || noDb
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const contextValue = {
|
|
519
|
+
isReady: authState.isAuthReady,
|
|
520
|
+
isSignedIn: authState.isSignedIn,
|
|
521
|
+
authStatus: authState.authStatus,
|
|
522
|
+
authErrorCode: authState.authErrorCode,
|
|
523
|
+
user: authState.user,
|
|
524
|
+
did: authState.did,
|
|
525
|
+
scope: authState.tokenScope,
|
|
526
|
+
hasScope: (s: string) => authRef.current.hasScope(s),
|
|
527
|
+
missingScopes: () => authRef.current.missingScopes(),
|
|
528
|
+
|
|
529
|
+
signIn: handleSignIn,
|
|
530
|
+
signInWithHandle: handleSignInWithHandle,
|
|
531
|
+
signOut: handleSignOut,
|
|
532
|
+
signInWithCode: (code: string, state?: string) =>
|
|
533
|
+
authRef.current.signInWithCode(code, state),
|
|
534
|
+
|
|
535
|
+
getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
|
|
536
|
+
getSignInUrl: (redirectUri?: string) =>
|
|
537
|
+
authRef.current.getSignInUrl(redirectUri),
|
|
538
|
+
|
|
539
|
+
db: getCurrentDb(),
|
|
540
|
+
dbStatus,
|
|
541
|
+
dbMode,
|
|
542
|
+
|
|
543
|
+
devInfo: schemaDevInfo,
|
|
544
|
+
refreshSchemaStatus,
|
|
545
|
+
|
|
546
|
+
isAuthReady: authState.isAuthReady,
|
|
547
|
+
signin: handleSignIn,
|
|
548
|
+
signout: handleSignOut,
|
|
549
|
+
signinWithCode: (code: string, state?: string) =>
|
|
550
|
+
authRef.current.signInWithCode(code, state),
|
|
551
|
+
getSignInLink: (redirectUri?: string) =>
|
|
552
|
+
authRef.current.getSignInUrl(redirectUri),
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return (
|
|
556
|
+
<BasicContext.Provider value={contextValue}>
|
|
557
|
+
{error && isDevMode() && <ErrorDisplay error={error} />}
|
|
558
|
+
{devToolbar && isDevMode() && (
|
|
559
|
+
<Suspense fallback={null}>
|
|
560
|
+
<BasicDevToolbar debug={debug} />
|
|
561
|
+
</Suspense>
|
|
562
|
+
)}
|
|
563
|
+
{isDbReady && authState.isAuthReady && children}
|
|
564
|
+
</BasicContext.Provider>
|
|
565
|
+
)
|
|
464
566
|
}
|
|
465
567
|
|
|
466
568
|
function ErrorDisplay({ error }: { error: ErrorObject }) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
569
|
+
return (
|
|
570
|
+
<div
|
|
571
|
+
style={{
|
|
572
|
+
position: 'absolute',
|
|
573
|
+
top: 20,
|
|
574
|
+
left: 20,
|
|
575
|
+
color: 'black',
|
|
576
|
+
backgroundColor: '#f8d7da',
|
|
577
|
+
border: '1px solid #f5c6cb',
|
|
578
|
+
borderRadius: '4px',
|
|
579
|
+
padding: '20px',
|
|
580
|
+
maxWidth: '400px',
|
|
581
|
+
margin: '20px auto',
|
|
582
|
+
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
|
|
583
|
+
fontFamily: 'monospace',
|
|
584
|
+
}}
|
|
585
|
+
>
|
|
586
|
+
<h3 style={{ fontSize: '0.8rem', opacity: 0.8 }}>code: {error.code}</h3>
|
|
587
|
+
<h1 style={{ fontSize: '1.2rem', lineHeight: 1.5 }}>{error.title}</h1>
|
|
588
|
+
<p>{error.message}</p>
|
|
589
|
+
</div>
|
|
590
|
+
)
|
|
489
591
|
}
|