@basictech/react 0.8.0-beta.3 → 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 +6 -0
- package/dist/index.d.mts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +537 -141
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +545 -142
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/readme.md +99 -97
- package/src/AuthContext.tsx +513 -415
- package/src/context.tsx +19 -1
- package/src/core/auth/AuthManager.ts +1235 -746
- package/src/sync/syncProtocol.js +23 -7
package/src/AuthContext.tsx
CHANGED
|
@@ -1,493 +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
|
-
if (error.errorType === 'forbidden') {
|
|
272
|
-
log('403 Forbidden - user lacks required scope, not signing out')
|
|
273
|
-
return
|
|
274
|
-
}
|
|
275
|
-
handleSignOut()
|
|
276
|
-
},
|
|
277
|
-
})
|
|
278
|
-
setDbStatus(DBStatus.ONLINE)
|
|
279
|
-
setIsReady(true)
|
|
280
|
-
}
|
|
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
|
|
281
312
|
}
|
|
282
313
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
localVersion: schema?.version,
|
|
296
|
-
status: 'invalid',
|
|
297
|
-
valid: false,
|
|
298
|
-
lastCheckedAt: Date.now(),
|
|
299
|
-
error: errorMessage.trim() || undefined,
|
|
300
|
-
})
|
|
301
|
-
setError({
|
|
302
|
-
code: 'schema_invalid',
|
|
303
|
-
title: 'Basic Schema is invalid!',
|
|
304
|
-
message: errorMessage,
|
|
305
|
-
})
|
|
306
|
-
setIsReady(true)
|
|
307
|
-
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
|
|
308
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
|
+
}
|
|
309
341
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
localVersion: schema?.version,
|
|
313
|
-
status: result.schemaStatus.status ?? 'unknown',
|
|
314
|
-
valid: result.schemaStatus.valid,
|
|
315
|
-
lastCheckedAt: Date.now(),
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
if (dbMode === 'remote') {
|
|
319
|
-
initRemoteDb()
|
|
320
|
-
} else {
|
|
321
|
-
if (result.schemaStatus.valid) {
|
|
322
|
-
await initSyncDb({ shouldConnect: true })
|
|
323
|
-
} else {
|
|
324
|
-
if (result.schemaStatus.status === 'unpublished') {
|
|
325
|
-
log('Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync.')
|
|
326
|
-
} else {
|
|
327
|
-
log('Schema is invalid!', result.schemaStatus)
|
|
328
|
-
}
|
|
329
|
-
await initSyncDb({ shouldConnect: false })
|
|
330
|
-
}
|
|
331
|
-
}
|
|
342
|
+
async function checkSchema() {
|
|
343
|
+
const result = await validateAndCheckSchema(schema)
|
|
332
344
|
|
|
333
|
-
|
|
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
|
+
})
|
|
334
351
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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 })
|
|
338
382
|
} else {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
projectId: project_id,
|
|
343
|
-
localVersion: undefined,
|
|
344
|
-
status: 'no_schema',
|
|
345
|
-
valid: false,
|
|
346
|
-
lastCheckedAt: Date.now(),
|
|
347
|
-
}
|
|
348
|
-
: 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.',
|
|
349
386
|
)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
387
|
+
} else {
|
|
388
|
+
log('Schema is invalid!', result.schemaStatus)
|
|
389
|
+
}
|
|
390
|
+
await initSyncDb({ shouldConnect: false })
|
|
355
391
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
useEffect(() => {
|
|
359
|
-
if (authState.hasToken && syncRef.current && authState.isSignedIn && shouldConnect) {
|
|
360
|
-
log('connecting to db...')
|
|
361
|
-
|
|
362
|
-
syncRef.current
|
|
363
|
-
?.connect({
|
|
364
|
-
getToken: (opts?: GetTokenOptions) => authRef.current.getToken(opts),
|
|
365
|
-
ws_url: authConfig.ws_url,
|
|
366
|
-
})
|
|
367
|
-
.catch((e: any) => {
|
|
368
|
-
log('error connecting to db', e)
|
|
369
|
-
})
|
|
370
|
-
}
|
|
371
|
-
}, [authState.isSignedIn, authState.hasToken, shouldConnect])
|
|
372
|
-
|
|
373
|
-
const handleSignOut = async () => {
|
|
374
|
-
await authRef.current.signOut()
|
|
375
|
-
if (syncRef.current) {
|
|
376
|
-
try {
|
|
377
|
-
await syncRef.current.close()
|
|
378
|
-
await syncRef.current.delete({ disableAutoOpen: false })
|
|
379
|
-
syncRef.current = null
|
|
380
|
-
window?.location?.reload()
|
|
381
|
-
} catch (error) {
|
|
382
|
-
console.error('Error during database cleanup:', error)
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
392
|
+
}
|
|
386
393
|
|
|
387
|
-
|
|
388
|
-
try {
|
|
389
|
-
await authRef.current.signIn()
|
|
390
|
-
} catch (error) {
|
|
391
|
-
if (isDevMode()) {
|
|
392
|
-
setError({
|
|
393
|
-
code: 'signin_error',
|
|
394
|
-
title: 'Sign-in Failed',
|
|
395
|
-
message:
|
|
396
|
-
(error as Error).message || 'An error occurred during sign-in. Please try again.',
|
|
397
|
-
})
|
|
398
|
-
}
|
|
399
|
-
throw error
|
|
400
|
-
}
|
|
394
|
+
checkForNewVersion()
|
|
401
395
|
}
|
|
402
396
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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(),
|
|
414
408
|
}
|
|
415
|
-
|
|
416
|
-
|
|
409
|
+
: null,
|
|
410
|
+
)
|
|
411
|
+
if (dbMode === 'remote' && project_id) {
|
|
412
|
+
initRemoteDb()
|
|
413
|
+
} else {
|
|
414
|
+
setIsDbReady(true)
|
|
415
|
+
}
|
|
417
416
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
|
424
448
|
}
|
|
425
449
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
isAuthReady: authState.isAuthReady,
|
|
451
|
-
signin: handleSignIn,
|
|
452
|
-
signout: handleSignOut,
|
|
453
|
-
signinWithCode: (code: string, state?: string) => authRef.current.signInWithCode(code, state),
|
|
454
|
-
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()
|
|
455
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
|
+
}
|
|
456
510
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
+
)
|
|
468
566
|
}
|
|
469
567
|
|
|
470
568
|
function ErrorDisplay({ error }: { error: ErrorObject }) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
+
)
|
|
493
591
|
}
|