@basictech/react 0.7.0 → 0.8.0-beta.2
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/.turbo/turbo-build.log +13 -12
- package/AUTH_IMPLEMENTATION_GUIDE.md +20 -18
- package/changelog.md +24 -2
- package/dist/index.d.mts +121 -48
- package/dist/index.d.ts +121 -48
- package/dist/index.js +1996 -758
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1981 -749
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
- package/readme.md +50 -1
- package/src/AuthContext.tsx +294 -818
- package/src/config.ts +1 -19
- package/src/context.tsx +104 -0
- package/src/core/auth/AuthManager.ts +858 -0
- package/src/core/db/RemoteCollection.ts +30 -16
- package/src/core/db/index.ts +1 -1
- package/src/core/db/types.ts +13 -1
- package/src/dev/BasicDevToolbar.tsx +665 -0
- package/src/index.ts +10 -3
- package/src/sync/index.ts +15 -29
- package/src/sync/syncProtocol.js +84 -22
- package/src/sync/tokenRegistry.ts +20 -0
- package/src/updater/updateMigrations.ts +3 -3
- package/src/updater/versionUpdater.ts +3 -10
- package/src/utils/network.ts +68 -15
- package/src/utils/normalizeClientId.ts +22 -0
- package/src/utils/resolveDid.ts +101 -0
- package/src/utils/schema.ts +3 -4
- package/src/utils/storage.ts +4 -1
package/dist/index.mjs
CHANGED
|
@@ -24,25 +24,53 @@ var init_config = __esm({
|
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
// src/sync/tokenRegistry.ts
|
|
28
|
+
function setTokenGetter(url, fn) {
|
|
29
|
+
registry.set(url, fn);
|
|
30
|
+
}
|
|
31
|
+
function getTokenGetter(url) {
|
|
32
|
+
return registry.get(url);
|
|
33
|
+
}
|
|
34
|
+
var registry;
|
|
35
|
+
var init_tokenRegistry = __esm({
|
|
36
|
+
"src/sync/tokenRegistry.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
registry = /* @__PURE__ */ new Map();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
27
42
|
// src/sync/syncProtocol.js
|
|
28
43
|
var syncProtocol_exports = {};
|
|
29
44
|
__export(syncProtocol_exports, {
|
|
30
45
|
syncProtocol: () => syncProtocol
|
|
31
46
|
});
|
|
32
47
|
import { Dexie } from "dexie";
|
|
48
|
+
function decodeJwtExp(token) {
|
|
49
|
+
try {
|
|
50
|
+
var parts = token.split(".");
|
|
51
|
+
if (parts.length !== 3) return null;
|
|
52
|
+
var payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
53
|
+
return typeof payload.exp === "number" ? payload.exp : null;
|
|
54
|
+
} catch (_) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
33
58
|
var syncProtocol;
|
|
34
59
|
var init_syncProtocol = __esm({
|
|
35
60
|
"src/sync/syncProtocol.js"() {
|
|
36
61
|
"use strict";
|
|
37
62
|
"use client";
|
|
38
63
|
init_config();
|
|
64
|
+
init_tokenRegistry();
|
|
39
65
|
syncProtocol = function() {
|
|
40
66
|
log("Initializing syncProtocol");
|
|
41
67
|
var RECONNECT_DELAY = 5e3;
|
|
68
|
+
var TOKEN_REFRESH_BUFFER = 60;
|
|
42
69
|
Dexie.Syncable.registerSyncProtocol("websocket", {
|
|
43
70
|
sync: function(context, url, options, baseRevision, syncedRevision, changes, partial, applyRemoteChanges, onChangesAccepted, onSuccess, onError) {
|
|
44
71
|
var requestId = 0;
|
|
45
72
|
var acceptCallbacks = {};
|
|
73
|
+
var refreshTimer = null;
|
|
46
74
|
log("Connecting to", url);
|
|
47
75
|
var ws = new WebSocket(url);
|
|
48
76
|
function sendChanges(changes2, baseRevision2, partial2, onChangesAccepted2) {
|
|
@@ -59,30 +87,71 @@ var init_syncProtocol = __esm({
|
|
|
59
87
|
})
|
|
60
88
|
);
|
|
61
89
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
function clearRefreshTimer() {
|
|
91
|
+
if (refreshTimer) {
|
|
92
|
+
clearTimeout(refreshTimer);
|
|
93
|
+
refreshTimer = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function resolveGetToken() {
|
|
97
|
+
var fn = getTokenGetter(url);
|
|
98
|
+
if (!fn) throw new Error("No token getter registered for " + url);
|
|
99
|
+
return fn;
|
|
100
|
+
}
|
|
101
|
+
function scheduleTokenRefresh(tokenStr) {
|
|
102
|
+
clearRefreshTimer();
|
|
103
|
+
var exp = decodeJwtExp(tokenStr);
|
|
104
|
+
if (!exp) return;
|
|
105
|
+
var msUntilRefresh = (exp - TOKEN_REFRESH_BUFFER) * 1e3 - Date.now();
|
|
106
|
+
if (msUntilRefresh <= 0) return;
|
|
107
|
+
log("Scheduling proactive token refresh in", Math.round(msUntilRefresh / 1e3), "s");
|
|
108
|
+
refreshTimer = setTimeout(async function() {
|
|
109
|
+
try {
|
|
110
|
+
var newToken = await resolveGetToken()({ forceRefresh: true });
|
|
111
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
112
|
+
log("Sending tokenUpdate on existing WebSocket");
|
|
113
|
+
ws.send(JSON.stringify({ type: "tokenUpdate", authToken: newToken }));
|
|
114
|
+
scheduleTokenRefresh(newToken);
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log("Proactive token refresh failed (non-fatal):", err);
|
|
118
|
+
}
|
|
119
|
+
}, msUntilRefresh);
|
|
120
|
+
}
|
|
121
|
+
ws.onopen = async function(event) {
|
|
122
|
+
try {
|
|
123
|
+
var token = await resolveGetToken()();
|
|
124
|
+
log("Opening socket - sending clientIdentity", context.clientIdentity);
|
|
125
|
+
ws.send(
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
type: "clientIdentity",
|
|
128
|
+
clientIdentity: context.clientIdentity || null,
|
|
129
|
+
authToken: token,
|
|
130
|
+
schema: options.schema
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
scheduleTokenRefresh(token);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
log("Failed to get token for WebSocket:", err);
|
|
136
|
+
ws.close();
|
|
137
|
+
onError("Authentication failed: " + (err.message || err), RECONNECT_DELAY);
|
|
138
|
+
}
|
|
72
139
|
};
|
|
73
140
|
ws.onerror = function(event) {
|
|
141
|
+
clearRefreshTimer();
|
|
74
142
|
ws.close();
|
|
75
143
|
log("ws.onerror", event);
|
|
76
144
|
onError(event?.message, RECONNECT_DELAY);
|
|
77
145
|
};
|
|
78
146
|
ws.onclose = function(event) {
|
|
147
|
+
clearRefreshTimer();
|
|
79
148
|
onError("Socket closed: " + event.reason, RECONNECT_DELAY);
|
|
80
149
|
};
|
|
81
150
|
var isFirstRound = true;
|
|
82
151
|
ws.onmessage = function(event) {
|
|
83
152
|
try {
|
|
84
153
|
var requestFromServer = JSON.parse(event.data);
|
|
85
|
-
log("requestFromServer", requestFromServer, {
|
|
154
|
+
log("requestFromServer", requestFromServer, { isFirstRound });
|
|
86
155
|
if (requestFromServer.type == "clientIdentity") {
|
|
87
156
|
context.clientIdentity = requestFromServer.clientIdentity;
|
|
88
157
|
context.save();
|
|
@@ -110,8 +179,8 @@ var init_syncProtocol = __esm({
|
|
|
110
179
|
onChangesAccepted2
|
|
111
180
|
);
|
|
112
181
|
},
|
|
113
|
-
// Specify a disconnect function that will close our socket so that we dont continue to monitor changes.
|
|
114
182
|
disconnect: function() {
|
|
183
|
+
clearRefreshTimer();
|
|
115
184
|
ws.close();
|
|
116
185
|
}
|
|
117
186
|
});
|
|
@@ -123,9 +192,13 @@ var init_syncProtocol = __esm({
|
|
|
123
192
|
acceptCallback();
|
|
124
193
|
delete acceptCallbacks[requestId2.toString()];
|
|
125
194
|
} else if (requestFromServer.type == "error") {
|
|
126
|
-
var requestId2 = requestFromServer.requestId;
|
|
127
195
|
ws.close();
|
|
128
|
-
|
|
196
|
+
if (requestFromServer.code === "TOKEN_EXPIRED" || requestFromServer.code === "UNAUTHORIZED") {
|
|
197
|
+
log("Auth error from server, will reconnect with fresh token:", requestFromServer.message);
|
|
198
|
+
onError(requestFromServer.message, RECONNECT_DELAY);
|
|
199
|
+
} else {
|
|
200
|
+
onError(requestFromServer.message, Infinity);
|
|
201
|
+
}
|
|
129
202
|
} else {
|
|
130
203
|
log("unknown message", requestFromServer);
|
|
131
204
|
ws.close();
|
|
@@ -143,24 +216,822 @@ var init_syncProtocol = __esm({
|
|
|
143
216
|
}
|
|
144
217
|
});
|
|
145
218
|
|
|
219
|
+
// package.json
|
|
220
|
+
var version;
|
|
221
|
+
var init_package = __esm({
|
|
222
|
+
"package.json"() {
|
|
223
|
+
version = "0.8.0-beta.2";
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// src/utils/network.ts
|
|
228
|
+
import semver from "semver";
|
|
229
|
+
function isDevelopment(debug) {
|
|
230
|
+
if (debug === true) return true;
|
|
231
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "development") return true;
|
|
232
|
+
if (typeof window === "undefined" || !window.location) return false;
|
|
233
|
+
const host = window.location.hostname;
|
|
234
|
+
return host === "localhost" || host === "127.0.0.1" || host.includes("localhost") || host.includes("127.0.0.1") || host.includes(".local");
|
|
235
|
+
}
|
|
236
|
+
function normalizeVersion(v) {
|
|
237
|
+
if (v == null) return null;
|
|
238
|
+
const t = String(v).trim();
|
|
239
|
+
return t.length ? t : null;
|
|
240
|
+
}
|
|
241
|
+
function versionsMatch(a, b) {
|
|
242
|
+
const na = a.trim();
|
|
243
|
+
const nb = b.trim();
|
|
244
|
+
if (na === nb) return true;
|
|
245
|
+
const va = semver.valid(na);
|
|
246
|
+
const vb = semver.valid(nb);
|
|
247
|
+
if (va && vb) return semver.eq(va, vb);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
function usesBetaDistTag(version2) {
|
|
251
|
+
const pre = semver.prerelease(version2);
|
|
252
|
+
const id = pre?.[0];
|
|
253
|
+
return typeof id === "string" && id.toLowerCase() === "beta";
|
|
254
|
+
}
|
|
255
|
+
async function checkForNewVersion() {
|
|
256
|
+
try {
|
|
257
|
+
const currentVersion = normalizeVersion(version);
|
|
258
|
+
if (!currentVersion) {
|
|
259
|
+
return { hasNewVersion: false, latestVersion: null, currentVersion: null };
|
|
260
|
+
}
|
|
261
|
+
const response = await fetch("https://registry.npmjs.org/@basictech/react", {
|
|
262
|
+
headers: { Accept: "application/vnd.npm.install-v1+json" }
|
|
263
|
+
});
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
throw new Error("Failed to fetch version from npm");
|
|
266
|
+
}
|
|
267
|
+
const data = await response.json();
|
|
268
|
+
const distTags = data["dist-tags"] ?? {};
|
|
269
|
+
const rawRegistry = usesBetaDistTag(currentVersion) ? distTags.beta ?? distTags.latest : distTags.latest;
|
|
270
|
+
const latestVersion = normalizeVersion(rawRegistry ?? null);
|
|
271
|
+
if (!latestVersion) {
|
|
272
|
+
throw new Error("Missing dist-tags from npm registry");
|
|
273
|
+
}
|
|
274
|
+
const same = versionsMatch(currentVersion, latestVersion);
|
|
275
|
+
if (!same && isDevelopment()) {
|
|
276
|
+
log("[basic] version check mismatch:", {
|
|
277
|
+
currentVersion,
|
|
278
|
+
registryVersion: latestVersion,
|
|
279
|
+
channel: usesBetaDistTag(currentVersion) ? "beta" : "latest"
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (!same) {
|
|
283
|
+
console.warn("[basic] New version available:", latestVersion, `
|
|
284
|
+
run "npm install @basictech/react@${latestVersion}" to update`);
|
|
285
|
+
}
|
|
286
|
+
if (usesBetaDistTag(currentVersion)) {
|
|
287
|
+
log("thank you for being on basictech/react beta :)");
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
hasNewVersion: !same,
|
|
291
|
+
latestVersion,
|
|
292
|
+
currentVersion
|
|
293
|
+
};
|
|
294
|
+
} catch (error) {
|
|
295
|
+
log("Error checking for new version:", error);
|
|
296
|
+
return {
|
|
297
|
+
hasNewVersion: false,
|
|
298
|
+
latestVersion: null,
|
|
299
|
+
currentVersion: null
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function cleanOAuthParamsFromUrl() {
|
|
304
|
+
if (window.location.search.includes("code") || window.location.search.includes("state")) {
|
|
305
|
+
const url = new URL(window.location.href);
|
|
306
|
+
url.searchParams.delete("code");
|
|
307
|
+
url.searchParams.delete("state");
|
|
308
|
+
window.history.pushState({}, document.title, url.pathname + url.search);
|
|
309
|
+
log("Cleaned OAuth parameters from URL");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function getSyncStatus(statusCode) {
|
|
313
|
+
switch (statusCode) {
|
|
314
|
+
case -1:
|
|
315
|
+
return "ERROR";
|
|
316
|
+
case 0:
|
|
317
|
+
return "OFFLINE";
|
|
318
|
+
case 1:
|
|
319
|
+
return "CONNECTING";
|
|
320
|
+
case 2:
|
|
321
|
+
return "ONLINE";
|
|
322
|
+
case 3:
|
|
323
|
+
return "SYNCING";
|
|
324
|
+
case 4:
|
|
325
|
+
return "ERROR_WILL_RETRY";
|
|
326
|
+
default:
|
|
327
|
+
return "UNKNOWN";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
var init_network = __esm({
|
|
331
|
+
"src/utils/network.ts"() {
|
|
332
|
+
"use strict";
|
|
333
|
+
init_config();
|
|
334
|
+
init_package();
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// src/context.tsx
|
|
339
|
+
import { createContext, useContext } from "react";
|
|
340
|
+
function useBasic() {
|
|
341
|
+
return useContext(BasicContext);
|
|
342
|
+
}
|
|
343
|
+
var DBStatus, noDb, BasicContext;
|
|
344
|
+
var init_context = __esm({
|
|
345
|
+
"src/context.tsx"() {
|
|
346
|
+
"use strict";
|
|
347
|
+
DBStatus = /* @__PURE__ */ ((DBStatus2) => {
|
|
348
|
+
DBStatus2["LOADING"] = "LOADING";
|
|
349
|
+
DBStatus2["OFFLINE"] = "OFFLINE";
|
|
350
|
+
DBStatus2["CONNECTING"] = "CONNECTING";
|
|
351
|
+
DBStatus2["ONLINE"] = "ONLINE";
|
|
352
|
+
DBStatus2["SYNCING"] = "SYNCING";
|
|
353
|
+
DBStatus2["ERROR"] = "ERROR";
|
|
354
|
+
DBStatus2["ERROR_WILL_RETRY"] = "ERROR_WILL_RETRY";
|
|
355
|
+
DBStatus2["ERROR_TOKEN_EXPIRED"] = "ERROR_TOKEN_EXPIRED";
|
|
356
|
+
return DBStatus2;
|
|
357
|
+
})(DBStatus || {});
|
|
358
|
+
noDb = {
|
|
359
|
+
collection: () => {
|
|
360
|
+
throw new Error("no basicdb found - initialization failed. double check your schema.");
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
BasicContext = createContext({
|
|
364
|
+
isReady: false,
|
|
365
|
+
isSignedIn: false,
|
|
366
|
+
user: null,
|
|
367
|
+
did: null,
|
|
368
|
+
scope: null,
|
|
369
|
+
hasScope: () => false,
|
|
370
|
+
missingScopes: () => [],
|
|
371
|
+
signIn: () => Promise.resolve(),
|
|
372
|
+
signInWithHandle: () => Promise.resolve(),
|
|
373
|
+
signOut: () => Promise.resolve(),
|
|
374
|
+
signInWithCode: () => Promise.resolve({ success: false }),
|
|
375
|
+
getToken: (_options) => Promise.reject(new Error("no token")),
|
|
376
|
+
getSignInUrl: () => Promise.resolve(""),
|
|
377
|
+
db: noDb,
|
|
378
|
+
dbStatus: "LOADING" /* LOADING */,
|
|
379
|
+
dbMode: "sync",
|
|
380
|
+
devInfo: null,
|
|
381
|
+
refreshSchemaStatus: async () => {
|
|
382
|
+
},
|
|
383
|
+
isAuthReady: false,
|
|
384
|
+
signin: () => Promise.resolve(),
|
|
385
|
+
signout: () => Promise.resolve(),
|
|
386
|
+
signinWithCode: () => Promise.resolve({ success: false }),
|
|
387
|
+
getSignInLink: () => Promise.resolve("")
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// src/dev/BasicDevToolbar.tsx
|
|
393
|
+
var BasicDevToolbar_exports = {};
|
|
394
|
+
__export(BasicDevToolbar_exports, {
|
|
395
|
+
BasicDevToolbar: () => BasicDevToolbar
|
|
396
|
+
});
|
|
397
|
+
import { useCallback, useMemo, useState } from "react";
|
|
398
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
399
|
+
function toneForAuth(isReady, isSignedIn) {
|
|
400
|
+
if (!isReady) return "muted";
|
|
401
|
+
if (isSignedIn) return "ok";
|
|
402
|
+
return "warn";
|
|
403
|
+
}
|
|
404
|
+
function toneForDb(dbMode, dbStatus) {
|
|
405
|
+
if (dbMode === "remote") return dbStatus === "ONLINE" /* ONLINE */ ? "ok" : "warn";
|
|
406
|
+
if (dbStatus === "ONLINE" /* ONLINE */ || dbStatus === "SYNCING" /* SYNCING */) return "ok";
|
|
407
|
+
if (dbStatus === "CONNECTING" /* CONNECTING */ || dbStatus === "LOADING" /* LOADING */) return "warn";
|
|
408
|
+
if (dbStatus === "OFFLINE" /* OFFLINE */) return "muted";
|
|
409
|
+
return "bad";
|
|
410
|
+
}
|
|
411
|
+
function toneForSchema(info) {
|
|
412
|
+
if (!info) return "muted";
|
|
413
|
+
if (info.valid && info.status === "current") return "ok";
|
|
414
|
+
if (info.status === "unpublished") return "warn";
|
|
415
|
+
if (info.status === "no_schema") return "muted";
|
|
416
|
+
return "bad";
|
|
417
|
+
}
|
|
418
|
+
function dbStatusLabel(status) {
|
|
419
|
+
switch (status) {
|
|
420
|
+
case "LOADING" /* LOADING */:
|
|
421
|
+
return "Initializing";
|
|
422
|
+
case "OFFLINE" /* OFFLINE */:
|
|
423
|
+
return "Offline";
|
|
424
|
+
case "CONNECTING" /* CONNECTING */:
|
|
425
|
+
return "Connecting";
|
|
426
|
+
case "ONLINE" /* ONLINE */:
|
|
427
|
+
return "Connected";
|
|
428
|
+
case "SYNCING" /* SYNCING */:
|
|
429
|
+
return "Syncing";
|
|
430
|
+
case "ERROR" /* ERROR */:
|
|
431
|
+
return "Error";
|
|
432
|
+
case "ERROR_WILL_RETRY" /* ERROR_WILL_RETRY */:
|
|
433
|
+
return "Retrying";
|
|
434
|
+
case "ERROR_TOKEN_EXPIRED" /* ERROR_TOKEN_EXPIRED */:
|
|
435
|
+
return "Token refresh";
|
|
436
|
+
default:
|
|
437
|
+
return String(status);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function chipColor(tone) {
|
|
441
|
+
switch (tone) {
|
|
442
|
+
case "ok":
|
|
443
|
+
return "#22c55e";
|
|
444
|
+
case "warn":
|
|
445
|
+
return "#eab308";
|
|
446
|
+
case "bad":
|
|
447
|
+
return "#ef4444";
|
|
448
|
+
default:
|
|
449
|
+
return "#71717a";
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function displayDid(did) {
|
|
453
|
+
return did || "\u2014";
|
|
454
|
+
}
|
|
455
|
+
function displayUserLine(user) {
|
|
456
|
+
const parts = [];
|
|
457
|
+
if (user.sub) parts.push(`sub: ${user.sub}`);
|
|
458
|
+
if (user.email) parts.push(`email: ${user.email}`);
|
|
459
|
+
if (user.name) parts.push(`name: ${user.name}`);
|
|
460
|
+
return parts.length ? parts.join(" \xB7 ") : "\u2014";
|
|
461
|
+
}
|
|
462
|
+
function ClipboardIcon() {
|
|
463
|
+
return /* @__PURE__ */ jsxs(
|
|
464
|
+
"svg",
|
|
465
|
+
{
|
|
466
|
+
width: "14",
|
|
467
|
+
height: "14",
|
|
468
|
+
viewBox: "0 0 24 24",
|
|
469
|
+
fill: "none",
|
|
470
|
+
stroke: "currentColor",
|
|
471
|
+
strokeWidth: "2",
|
|
472
|
+
strokeLinecap: "round",
|
|
473
|
+
strokeLinejoin: "round",
|
|
474
|
+
"aria-hidden": true,
|
|
475
|
+
children: [
|
|
476
|
+
/* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
|
|
477
|
+
/* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
|
|
478
|
+
]
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
function SectionHeader({ children }) {
|
|
483
|
+
return /* @__PURE__ */ jsx(
|
|
484
|
+
"div",
|
|
485
|
+
{
|
|
486
|
+
style: {
|
|
487
|
+
fontSize: 10,
|
|
488
|
+
fontWeight: 700,
|
|
489
|
+
color: "#e4e4e7",
|
|
490
|
+
letterSpacing: "0.07em",
|
|
491
|
+
textTransform: "uppercase",
|
|
492
|
+
marginBottom: 8
|
|
493
|
+
},
|
|
494
|
+
children
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
function SectionRule() {
|
|
499
|
+
const bleed = PANEL_PAD_X;
|
|
500
|
+
return /* @__PURE__ */ jsx(
|
|
501
|
+
"div",
|
|
502
|
+
{
|
|
503
|
+
role: "separator",
|
|
504
|
+
style: {
|
|
505
|
+
height: 1,
|
|
506
|
+
background: "rgba(255, 255, 255, 0.055)",
|
|
507
|
+
marginLeft: -bleed,
|
|
508
|
+
marginRight: -bleed,
|
|
509
|
+
marginTop: 14,
|
|
510
|
+
marginBottom: 10,
|
|
511
|
+
width: `calc(100% + ${bleed * 2}px)`
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
function CopyableRow({
|
|
517
|
+
rowKey,
|
|
518
|
+
label,
|
|
519
|
+
copyText,
|
|
520
|
+
copiedKey,
|
|
521
|
+
onCopied,
|
|
522
|
+
children
|
|
523
|
+
}) {
|
|
524
|
+
const [hover, setHover] = useState(false);
|
|
525
|
+
const canCopy = copyText.length > 0;
|
|
526
|
+
const handleClick = useCallback(
|
|
527
|
+
(e) => {
|
|
528
|
+
e.stopPropagation();
|
|
529
|
+
if (!canCopy) return;
|
|
530
|
+
void navigator.clipboard.writeText(copyText).then(() => onCopied(rowKey));
|
|
531
|
+
},
|
|
532
|
+
[canCopy, copyText, onCopied, rowKey]
|
|
533
|
+
);
|
|
534
|
+
return /* @__PURE__ */ jsxs(
|
|
535
|
+
"div",
|
|
536
|
+
{
|
|
537
|
+
role: canCopy ? "button" : void 0,
|
|
538
|
+
tabIndex: canCopy ? 0 : void 0,
|
|
539
|
+
onClick: canCopy ? handleClick : void 0,
|
|
540
|
+
onKeyDown: canCopy ? (e) => {
|
|
541
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
542
|
+
e.preventDefault();
|
|
543
|
+
handleClick(e);
|
|
544
|
+
}
|
|
545
|
+
} : void 0,
|
|
546
|
+
onMouseEnter: () => setHover(true),
|
|
547
|
+
onMouseLeave: () => setHover(false),
|
|
548
|
+
style: {
|
|
549
|
+
display: "flex",
|
|
550
|
+
gap: 8,
|
|
551
|
+
marginBottom: 6,
|
|
552
|
+
alignItems: "flex-start",
|
|
553
|
+
borderRadius: 6,
|
|
554
|
+
padding: "4px 6px",
|
|
555
|
+
marginLeft: -6,
|
|
556
|
+
marginRight: -6,
|
|
557
|
+
cursor: canCopy ? "pointer" : "default",
|
|
558
|
+
background: hover && canCopy ? "rgba(255,255,255,0.06)" : "transparent",
|
|
559
|
+
transition: "background 0.12s ease"
|
|
560
|
+
},
|
|
561
|
+
children: [
|
|
562
|
+
/* @__PURE__ */ jsx("span", { style: { color: "#a1a1aa", minWidth: 88, flexShrink: 0, paddingTop: 2 }, children: label }),
|
|
563
|
+
/* @__PURE__ */ jsx(
|
|
564
|
+
"span",
|
|
565
|
+
{
|
|
566
|
+
style: {
|
|
567
|
+
flex: 1,
|
|
568
|
+
minWidth: 0,
|
|
569
|
+
wordBreak: "break-all",
|
|
570
|
+
paddingTop: 2,
|
|
571
|
+
lineHeight: 1.35
|
|
572
|
+
},
|
|
573
|
+
children
|
|
574
|
+
}
|
|
575
|
+
),
|
|
576
|
+
canCopy && /* @__PURE__ */ jsx(
|
|
577
|
+
"span",
|
|
578
|
+
{
|
|
579
|
+
style: {
|
|
580
|
+
flexShrink: 0,
|
|
581
|
+
color: copiedKey === rowKey ? "#22c55e" : "#71717a",
|
|
582
|
+
opacity: hover || copiedKey === rowKey ? 1 : 0,
|
|
583
|
+
transition: "opacity 0.12s ease, color 0.12s ease",
|
|
584
|
+
paddingTop: 2,
|
|
585
|
+
display: "flex",
|
|
586
|
+
alignItems: "flex-start"
|
|
587
|
+
},
|
|
588
|
+
title: "Copy value",
|
|
589
|
+
children: copiedKey === rowKey ? /* @__PURE__ */ jsx("span", { style: { fontSize: 10 }, children: "\u2713" }) : /* @__PURE__ */ jsx(ClipboardIcon, {})
|
|
590
|
+
}
|
|
591
|
+
)
|
|
592
|
+
]
|
|
593
|
+
}
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
function BasicDevToolbar({ enabled = true, debug }) {
|
|
597
|
+
const {
|
|
598
|
+
isReady,
|
|
599
|
+
isSignedIn,
|
|
600
|
+
user,
|
|
601
|
+
did,
|
|
602
|
+
scope,
|
|
603
|
+
missingScopes,
|
|
604
|
+
dbMode,
|
|
605
|
+
dbStatus,
|
|
606
|
+
devInfo,
|
|
607
|
+
refreshSchemaStatus
|
|
608
|
+
} = useBasic();
|
|
609
|
+
const [open, setOpen] = useState(false);
|
|
610
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
611
|
+
const [copied, setCopied] = useState(false);
|
|
612
|
+
const [rowCopied, setRowCopied] = useState(null);
|
|
613
|
+
const show = enabled && typeof window !== "undefined" && isDevelopment(debug);
|
|
614
|
+
const authTone = toneForAuth(isReady, isSignedIn);
|
|
615
|
+
const dbTone = toneForDb(dbMode, dbStatus);
|
|
616
|
+
const schemaTone = toneForSchema(devInfo);
|
|
617
|
+
const syncTone = dbMode === "remote" ? "muted" : dbTone === "ok" || dbStatus === "SYNCING" /* SYNCING */ ? "ok" : dbTone === "warn" ? "warn" : dbTone === "bad" ? "bad" : "muted";
|
|
618
|
+
const handleRefreshSchema = useCallback(async () => {
|
|
619
|
+
setRefreshing(true);
|
|
620
|
+
try {
|
|
621
|
+
await refreshSchemaStatus();
|
|
622
|
+
} finally {
|
|
623
|
+
setRefreshing(false);
|
|
624
|
+
}
|
|
625
|
+
}, [refreshSchemaStatus]);
|
|
626
|
+
const missingList = missingScopes();
|
|
627
|
+
const debugPayload = useMemo(() => {
|
|
628
|
+
return {
|
|
629
|
+
sdkVersion: version,
|
|
630
|
+
isReady,
|
|
631
|
+
isSignedIn,
|
|
632
|
+
did: did ?? null,
|
|
633
|
+
user: user ? {
|
|
634
|
+
sub: user.sub,
|
|
635
|
+
email: user.email,
|
|
636
|
+
name: user.name,
|
|
637
|
+
picture: user.picture
|
|
638
|
+
} : null,
|
|
639
|
+
scope,
|
|
640
|
+
missingScopes: missingList,
|
|
641
|
+
dbMode,
|
|
642
|
+
dbStatus,
|
|
643
|
+
indexedDbName: dbMode === "sync" ? INDEXED_DB_NAME : null,
|
|
644
|
+
schema: devInfo
|
|
645
|
+
};
|
|
646
|
+
}, [isReady, isSignedIn, did, user, scope, dbMode, dbStatus, devInfo, missingList]);
|
|
647
|
+
const handleCopy = useCallback(async () => {
|
|
648
|
+
try {
|
|
649
|
+
await navigator.clipboard.writeText(JSON.stringify(debugPayload, null, 2));
|
|
650
|
+
setCopied(true);
|
|
651
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
652
|
+
} catch {
|
|
653
|
+
}
|
|
654
|
+
}, [debugPayload]);
|
|
655
|
+
const onRowCopied = useCallback((key) => {
|
|
656
|
+
setRowCopied(key);
|
|
657
|
+
setTimeout(() => setRowCopied((k) => k === key ? null : k), 1500);
|
|
658
|
+
}, []);
|
|
659
|
+
if (!show) return null;
|
|
660
|
+
const shell = {
|
|
661
|
+
position: "fixed",
|
|
662
|
+
bottom: 12,
|
|
663
|
+
left: "50%",
|
|
664
|
+
transform: "translateX(-50%)",
|
|
665
|
+
zIndex: 99999,
|
|
666
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
|
667
|
+
fontSize: 11,
|
|
668
|
+
color: "#e4e4e7",
|
|
669
|
+
pointerEvents: "auto"
|
|
670
|
+
};
|
|
671
|
+
const bar = {
|
|
672
|
+
display: "flex",
|
|
673
|
+
alignItems: "center",
|
|
674
|
+
gap: 8,
|
|
675
|
+
padding: "8px 12px",
|
|
676
|
+
borderRadius: 999,
|
|
677
|
+
background: "rgba(24, 24, 27, 0.92)",
|
|
678
|
+
border: "1px solid rgba(63, 63, 70, 0.9)",
|
|
679
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.35)",
|
|
680
|
+
cursor: "pointer",
|
|
681
|
+
userSelect: "none"
|
|
682
|
+
};
|
|
683
|
+
const dot = (tone) => /* @__PURE__ */ jsx(
|
|
684
|
+
"span",
|
|
685
|
+
{
|
|
686
|
+
style: {
|
|
687
|
+
display: "block",
|
|
688
|
+
boxSizing: "border-box",
|
|
689
|
+
width: 6,
|
|
690
|
+
height: 6,
|
|
691
|
+
minWidth: 6,
|
|
692
|
+
minHeight: 6,
|
|
693
|
+
maxWidth: 6,
|
|
694
|
+
maxHeight: 6,
|
|
695
|
+
borderRadius: "50%",
|
|
696
|
+
background: chipColor(tone),
|
|
697
|
+
flexShrink: 0
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
const dotSlot = (title, tone) => /* @__PURE__ */ jsx(
|
|
702
|
+
"span",
|
|
703
|
+
{
|
|
704
|
+
title,
|
|
705
|
+
style: {
|
|
706
|
+
display: "inline-flex",
|
|
707
|
+
alignItems: "center",
|
|
708
|
+
justifyContent: "center",
|
|
709
|
+
width: 6,
|
|
710
|
+
height: 6,
|
|
711
|
+
flexShrink: 0,
|
|
712
|
+
lineHeight: 0
|
|
713
|
+
},
|
|
714
|
+
children: dot(tone)
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
const panel = {
|
|
718
|
+
marginBottom: 8,
|
|
719
|
+
maxHeight: "50vh",
|
|
720
|
+
overflow: "auto",
|
|
721
|
+
padding: PANEL_PAD_X,
|
|
722
|
+
borderRadius: 10,
|
|
723
|
+
background: "rgba(24, 24, 27, 0.96)",
|
|
724
|
+
border: "1px solid rgba(63, 63, 70, 0.9)",
|
|
725
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.35)",
|
|
726
|
+
minWidth: 300,
|
|
727
|
+
maxWidth: "min(560px, calc(100vw - 24px))"
|
|
728
|
+
};
|
|
729
|
+
const syncStatusText = dbStatusLabel(dbStatus);
|
|
730
|
+
return /* @__PURE__ */ jsxs("div", { style: shell, children: [
|
|
731
|
+
open && /* @__PURE__ */ jsxs("div", { style: panel, children: [
|
|
732
|
+
/* @__PURE__ */ jsxs("div", { style: { marginBottom: 12 }, children: [
|
|
733
|
+
/* @__PURE__ */ jsx("div", { style: { fontWeight: 600, fontSize: 12 }, children: "Basic SDK" }),
|
|
734
|
+
/* @__PURE__ */ jsxs("div", { style: { color: "#71717a", fontSize: 10, marginTop: 2 }, children: [
|
|
735
|
+
"v",
|
|
736
|
+
version
|
|
737
|
+
] })
|
|
738
|
+
] }),
|
|
739
|
+
/* @__PURE__ */ jsx(SectionHeader, { children: "Auth" }),
|
|
740
|
+
/* @__PURE__ */ jsx(
|
|
741
|
+
CopyableRow,
|
|
742
|
+
{
|
|
743
|
+
rowKey: "ready",
|
|
744
|
+
label: "Ready",
|
|
745
|
+
copyText: String(isReady),
|
|
746
|
+
copiedKey: rowCopied,
|
|
747
|
+
onCopied: onRowCopied,
|
|
748
|
+
children: String(isReady)
|
|
749
|
+
}
|
|
750
|
+
),
|
|
751
|
+
/* @__PURE__ */ jsx(
|
|
752
|
+
CopyableRow,
|
|
753
|
+
{
|
|
754
|
+
rowKey: "signedIn",
|
|
755
|
+
label: "Signed in",
|
|
756
|
+
copyText: String(isSignedIn),
|
|
757
|
+
copiedKey: rowCopied,
|
|
758
|
+
onCopied: onRowCopied,
|
|
759
|
+
children: String(isSignedIn)
|
|
760
|
+
}
|
|
761
|
+
),
|
|
762
|
+
/* @__PURE__ */ jsx(
|
|
763
|
+
CopyableRow,
|
|
764
|
+
{
|
|
765
|
+
rowKey: "did",
|
|
766
|
+
label: "DID",
|
|
767
|
+
copyText: did || "",
|
|
768
|
+
copiedKey: rowCopied,
|
|
769
|
+
onCopied: onRowCopied,
|
|
770
|
+
children: displayDid(did)
|
|
771
|
+
}
|
|
772
|
+
),
|
|
773
|
+
/* @__PURE__ */ jsx(
|
|
774
|
+
CopyableRow,
|
|
775
|
+
{
|
|
776
|
+
rowKey: "user",
|
|
777
|
+
label: "User",
|
|
778
|
+
copyText: user ? displayUserLine(user) : "",
|
|
779
|
+
copiedKey: rowCopied,
|
|
780
|
+
onCopied: onRowCopied,
|
|
781
|
+
children: user ? displayUserLine(user) : "\u2014"
|
|
782
|
+
}
|
|
783
|
+
),
|
|
784
|
+
/* @__PURE__ */ jsx(
|
|
785
|
+
CopyableRow,
|
|
786
|
+
{
|
|
787
|
+
rowKey: "scopes",
|
|
788
|
+
label: "Scopes",
|
|
789
|
+
copyText: scope || "",
|
|
790
|
+
copiedKey: rowCopied,
|
|
791
|
+
onCopied: onRowCopied,
|
|
792
|
+
children: scope || "\u2014"
|
|
793
|
+
}
|
|
794
|
+
),
|
|
795
|
+
/* @__PURE__ */ jsx(
|
|
796
|
+
CopyableRow,
|
|
797
|
+
{
|
|
798
|
+
rowKey: "missingScopes",
|
|
799
|
+
label: "Missing scopes",
|
|
800
|
+
copyText: missingList.length ? missingList.join(", ") : "",
|
|
801
|
+
copiedKey: rowCopied,
|
|
802
|
+
onCopied: onRowCopied,
|
|
803
|
+
children: missingList.length ? missingList.join(", ") : "\u2014"
|
|
804
|
+
}
|
|
805
|
+
),
|
|
806
|
+
/* @__PURE__ */ jsx(SectionRule, {}),
|
|
807
|
+
/* @__PURE__ */ jsx(SectionHeader, { children: "Database" }),
|
|
808
|
+
/* @__PURE__ */ jsx(
|
|
809
|
+
CopyableRow,
|
|
810
|
+
{
|
|
811
|
+
rowKey: "dbMode",
|
|
812
|
+
label: "Mode",
|
|
813
|
+
copyText: dbMode,
|
|
814
|
+
copiedKey: rowCopied,
|
|
815
|
+
onCopied: onRowCopied,
|
|
816
|
+
children: dbMode
|
|
817
|
+
}
|
|
818
|
+
),
|
|
819
|
+
/* @__PURE__ */ jsx(
|
|
820
|
+
CopyableRow,
|
|
821
|
+
{
|
|
822
|
+
rowKey: "indexedDb",
|
|
823
|
+
label: "IndexedDB",
|
|
824
|
+
copyText: dbMode === "sync" ? INDEXED_DB_NAME : "",
|
|
825
|
+
copiedKey: rowCopied,
|
|
826
|
+
onCopied: onRowCopied,
|
|
827
|
+
children: dbMode === "sync" ? INDEXED_DB_NAME : "\u2014"
|
|
828
|
+
}
|
|
829
|
+
),
|
|
830
|
+
/* @__PURE__ */ jsx(
|
|
831
|
+
CopyableRow,
|
|
832
|
+
{
|
|
833
|
+
rowKey: "syncStatus",
|
|
834
|
+
label: "Sync / status",
|
|
835
|
+
copyText: syncStatusText,
|
|
836
|
+
copiedKey: rowCopied,
|
|
837
|
+
onCopied: onRowCopied,
|
|
838
|
+
children: syncStatusText
|
|
839
|
+
}
|
|
840
|
+
),
|
|
841
|
+
/* @__PURE__ */ jsx(SectionRule, {}),
|
|
842
|
+
/* @__PURE__ */ jsx(SectionHeader, { children: "Schema" }),
|
|
843
|
+
devInfo ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
844
|
+
/* @__PURE__ */ jsx(
|
|
845
|
+
CopyableRow,
|
|
846
|
+
{
|
|
847
|
+
rowKey: "schemaProject",
|
|
848
|
+
label: "Project",
|
|
849
|
+
copyText: devInfo.projectId ?? "",
|
|
850
|
+
copiedKey: rowCopied,
|
|
851
|
+
onCopied: onRowCopied,
|
|
852
|
+
children: devInfo.projectId ?? "\u2014"
|
|
853
|
+
}
|
|
854
|
+
),
|
|
855
|
+
/* @__PURE__ */ jsx(
|
|
856
|
+
CopyableRow,
|
|
857
|
+
{
|
|
858
|
+
rowKey: "schemaLocalVer",
|
|
859
|
+
label: "Local version",
|
|
860
|
+
copyText: devInfo.localVersion !== void 0 && devInfo.localVersion !== null ? String(devInfo.localVersion) : "",
|
|
861
|
+
copiedKey: rowCopied,
|
|
862
|
+
onCopied: onRowCopied,
|
|
863
|
+
children: devInfo.localVersion ?? "\u2014"
|
|
864
|
+
}
|
|
865
|
+
),
|
|
866
|
+
/* @__PURE__ */ jsx(
|
|
867
|
+
CopyableRow,
|
|
868
|
+
{
|
|
869
|
+
rowKey: "schemaRemote",
|
|
870
|
+
label: "Remote check",
|
|
871
|
+
copyText: devInfo.status,
|
|
872
|
+
copiedKey: rowCopied,
|
|
873
|
+
onCopied: onRowCopied,
|
|
874
|
+
children: devInfo.status
|
|
875
|
+
}
|
|
876
|
+
),
|
|
877
|
+
/* @__PURE__ */ jsx(
|
|
878
|
+
CopyableRow,
|
|
879
|
+
{
|
|
880
|
+
rowKey: "schemaValid",
|
|
881
|
+
label: "Valid",
|
|
882
|
+
copyText: String(devInfo.valid),
|
|
883
|
+
copiedKey: rowCopied,
|
|
884
|
+
onCopied: onRowCopied,
|
|
885
|
+
children: String(devInfo.valid)
|
|
886
|
+
}
|
|
887
|
+
),
|
|
888
|
+
/* @__PURE__ */ jsx(
|
|
889
|
+
CopyableRow,
|
|
890
|
+
{
|
|
891
|
+
rowKey: "schemaChecked",
|
|
892
|
+
label: "Checked",
|
|
893
|
+
copyText: devInfo.lastCheckedAt ? new Date(devInfo.lastCheckedAt).toISOString() : "",
|
|
894
|
+
copiedKey: rowCopied,
|
|
895
|
+
onCopied: onRowCopied,
|
|
896
|
+
children: devInfo.lastCheckedAt ? new Date(devInfo.lastCheckedAt).toLocaleString() : "\u2014"
|
|
897
|
+
}
|
|
898
|
+
),
|
|
899
|
+
devInfo.error ? /* @__PURE__ */ jsx(
|
|
900
|
+
CopyableRow,
|
|
901
|
+
{
|
|
902
|
+
rowKey: "schemaError",
|
|
903
|
+
label: "Error",
|
|
904
|
+
copyText: devInfo.error,
|
|
905
|
+
copiedKey: rowCopied,
|
|
906
|
+
onCopied: onRowCopied,
|
|
907
|
+
children: devInfo.error
|
|
908
|
+
}
|
|
909
|
+
) : null
|
|
910
|
+
] }) : /* @__PURE__ */ jsx(
|
|
911
|
+
CopyableRow,
|
|
912
|
+
{
|
|
913
|
+
rowKey: "schemaStatus",
|
|
914
|
+
label: "Status",
|
|
915
|
+
copyText: "No schema on provider",
|
|
916
|
+
copiedKey: rowCopied,
|
|
917
|
+
onCopied: onRowCopied,
|
|
918
|
+
children: "No schema on provider"
|
|
919
|
+
}
|
|
920
|
+
),
|
|
921
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, marginTop: 12, flexWrap: "wrap" }, children: [
|
|
922
|
+
/* @__PURE__ */ jsx(
|
|
923
|
+
"button",
|
|
924
|
+
{
|
|
925
|
+
type: "button",
|
|
926
|
+
onClick: (e) => {
|
|
927
|
+
e.stopPropagation();
|
|
928
|
+
void handleRefreshSchema();
|
|
929
|
+
},
|
|
930
|
+
disabled: refreshing,
|
|
931
|
+
style: {
|
|
932
|
+
padding: "6px 10px",
|
|
933
|
+
borderRadius: 6,
|
|
934
|
+
border: "1px solid #3f3f46",
|
|
935
|
+
background: "#27272a",
|
|
936
|
+
color: "#e4e4e7",
|
|
937
|
+
cursor: refreshing ? "wait" : "pointer",
|
|
938
|
+
fontSize: 11,
|
|
939
|
+
fontFamily: "inherit"
|
|
940
|
+
},
|
|
941
|
+
children: refreshing ? "Refreshing\u2026" : "Refresh schema"
|
|
942
|
+
}
|
|
943
|
+
),
|
|
944
|
+
/* @__PURE__ */ jsx(
|
|
945
|
+
"button",
|
|
946
|
+
{
|
|
947
|
+
type: "button",
|
|
948
|
+
onClick: (e) => {
|
|
949
|
+
e.stopPropagation();
|
|
950
|
+
void handleCopy();
|
|
951
|
+
},
|
|
952
|
+
style: {
|
|
953
|
+
padding: "6px 10px",
|
|
954
|
+
borderRadius: 6,
|
|
955
|
+
border: "1px solid #3f3f46",
|
|
956
|
+
background: "#27272a",
|
|
957
|
+
color: "#e4e4e7",
|
|
958
|
+
cursor: "pointer",
|
|
959
|
+
fontSize: 11,
|
|
960
|
+
fontFamily: "inherit"
|
|
961
|
+
},
|
|
962
|
+
children: copied ? "Copied" : "Copy debug info"
|
|
963
|
+
}
|
|
964
|
+
)
|
|
965
|
+
] })
|
|
966
|
+
] }),
|
|
967
|
+
/* @__PURE__ */ jsxs(
|
|
968
|
+
"button",
|
|
969
|
+
{
|
|
970
|
+
type: "button",
|
|
971
|
+
"aria-expanded": open,
|
|
972
|
+
onClick: () => setOpen((o) => !o),
|
|
973
|
+
style: {
|
|
974
|
+
...bar,
|
|
975
|
+
border: "none",
|
|
976
|
+
width: "100%",
|
|
977
|
+
cursor: "pointer"
|
|
978
|
+
},
|
|
979
|
+
children: [
|
|
980
|
+
/* @__PURE__ */ jsx("span", { style: { fontWeight: 600, letterSpacing: 0.02 }, children: "Basic" }),
|
|
981
|
+
/* @__PURE__ */ jsxs(
|
|
982
|
+
"span",
|
|
983
|
+
{
|
|
984
|
+
style: {
|
|
985
|
+
display: "inline-flex",
|
|
986
|
+
alignItems: "center",
|
|
987
|
+
gap: 6,
|
|
988
|
+
marginLeft: 8,
|
|
989
|
+
height: 6,
|
|
990
|
+
flexShrink: 0,
|
|
991
|
+
lineHeight: 0
|
|
992
|
+
},
|
|
993
|
+
children: [
|
|
994
|
+
dotSlot("Auth", authTone),
|
|
995
|
+
dotSlot("DB", dbTone),
|
|
996
|
+
dotSlot("Sync", syncTone),
|
|
997
|
+
dotSlot("Schema", schemaTone)
|
|
998
|
+
]
|
|
999
|
+
}
|
|
1000
|
+
),
|
|
1001
|
+
/* @__PURE__ */ jsx("span", { style: { color: "#71717a", marginLeft: 4 }, children: open ? "\u25BE" : "\u25B4" })
|
|
1002
|
+
]
|
|
1003
|
+
}
|
|
1004
|
+
)
|
|
1005
|
+
] });
|
|
1006
|
+
}
|
|
1007
|
+
var INDEXED_DB_NAME, PANEL_PAD_X;
|
|
1008
|
+
var init_BasicDevToolbar = __esm({
|
|
1009
|
+
"src/dev/BasicDevToolbar.tsx"() {
|
|
1010
|
+
"use strict";
|
|
1011
|
+
"use client";
|
|
1012
|
+
init_context();
|
|
1013
|
+
init_package();
|
|
1014
|
+
init_network();
|
|
1015
|
+
INDEXED_DB_NAME = "basicdb";
|
|
1016
|
+
PANEL_PAD_X = 12;
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
|
|
146
1020
|
// src/AuthContext.tsx
|
|
147
|
-
import {
|
|
148
|
-
import { jwtDecode } from "jwt-decode";
|
|
1021
|
+
import { useCallback as useCallback2, useEffect, useRef, useState as useState2, Suspense, lazy } from "react";
|
|
149
1022
|
|
|
150
1023
|
// src/sync/index.ts
|
|
151
1024
|
init_config();
|
|
1025
|
+
init_tokenRegistry();
|
|
152
1026
|
import { v7 as uuidv7 } from "uuid";
|
|
153
1027
|
import { Dexie as Dexie2 } from "dexie";
|
|
154
1028
|
import { validateData } from "@basictech/schema";
|
|
155
1029
|
var dexieExtensionsLoaded = false;
|
|
156
1030
|
var initPromise = null;
|
|
157
1031
|
async function initDexieExtensions() {
|
|
158
|
-
if (dexieExtensionsLoaded)
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
return;
|
|
162
|
-
if (initPromise)
|
|
163
|
-
return initPromise;
|
|
1032
|
+
if (dexieExtensionsLoaded) return;
|
|
1033
|
+
if (typeof window === "undefined") return;
|
|
1034
|
+
if (initPromise) return initPromise;
|
|
164
1035
|
initPromise = (async () => {
|
|
165
1036
|
try {
|
|
166
1037
|
await import("dexie-syncable");
|
|
@@ -185,12 +1056,13 @@ var BasicSync = class extends Dexie2 {
|
|
|
185
1056
|
this.version(2).stores({});
|
|
186
1057
|
this.Collection.prototype.get = this.Collection.prototype.toArray;
|
|
187
1058
|
}
|
|
188
|
-
async connect({
|
|
1059
|
+
async connect({ getToken, ws_url }) {
|
|
189
1060
|
const WS_URL = ws_url || "wss://pds.basic.id/ws";
|
|
190
1061
|
log("Connecting to", WS_URL);
|
|
1062
|
+
setTokenGetter(WS_URL, getToken);
|
|
191
1063
|
await this.updateSyncNodes();
|
|
192
1064
|
log("Starting connection...");
|
|
193
|
-
return this.syncable.connect("websocket", WS_URL, {
|
|
1065
|
+
return this.syncable.connect("websocket", WS_URL, { schema: this.basic_schema });
|
|
194
1066
|
}
|
|
195
1067
|
async disconnect({ ws_url } = {}) {
|
|
196
1068
|
const WS_URL = ws_url || "wss://pds.basic.id/ws";
|
|
@@ -230,7 +1102,7 @@ var BasicSync = class extends Dexie2 {
|
|
|
230
1102
|
}
|
|
231
1103
|
_convertSchemaToDxSchema(schema) {
|
|
232
1104
|
const stores = Object.entries(schema.tables).map(([key, table]) => {
|
|
233
|
-
const indexedFields = Object.entries(table.fields).filter(([
|
|
1105
|
+
const indexedFields = Object.entries(table.fields).filter(([, field]) => field.indexed).map(([fieldKey]) => `,${fieldKey}`).join("");
|
|
234
1106
|
return {
|
|
235
1107
|
[key]: "id" + indexedFields
|
|
236
1108
|
};
|
|
@@ -397,29 +1269,45 @@ var RemoteCollection = class {
|
|
|
397
1269
|
const token = await this.config.getToken();
|
|
398
1270
|
const url = `${this.config.serverUrl}${path}`;
|
|
399
1271
|
this.log(`${method} ${url}`, body ? JSON.stringify(body) : "");
|
|
1272
|
+
const headers = {
|
|
1273
|
+
"Authorization": `Bearer ${token}`
|
|
1274
|
+
};
|
|
1275
|
+
if (body) {
|
|
1276
|
+
headers["Content-Type"] = "application/json";
|
|
1277
|
+
}
|
|
400
1278
|
const response = await fetch(url, {
|
|
401
1279
|
method,
|
|
402
|
-
headers
|
|
403
|
-
"Content-Type": "application/json",
|
|
404
|
-
"Authorization": `Bearer ${token}`
|
|
405
|
-
},
|
|
1280
|
+
headers,
|
|
406
1281
|
...body ? { body: JSON.stringify(body) } : {}
|
|
407
1282
|
});
|
|
408
1283
|
const responseData = await response.json().catch(() => ({}));
|
|
409
1284
|
if (!response.ok) {
|
|
410
1285
|
if (response.status === 401 && !isRetry) {
|
|
411
|
-
this.log("Got 401,
|
|
1286
|
+
this.log("Got 401, forcing token refresh and retrying...");
|
|
1287
|
+
await this.config.getToken({ forceRefresh: true });
|
|
412
1288
|
return this.request(method, path, body, true);
|
|
413
1289
|
}
|
|
414
1290
|
if (this.config.debug) {
|
|
415
1291
|
console.error(`[RemoteDB] Error ${response.status}:`, responseData);
|
|
416
1292
|
}
|
|
417
|
-
if (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
1293
|
+
if (this.config.onAuthError) {
|
|
1294
|
+
if (response.status === 401) {
|
|
1295
|
+
this.config.onAuthError({
|
|
1296
|
+
status: response.status,
|
|
1297
|
+
message: "Authentication failed",
|
|
1298
|
+
response: responseData,
|
|
1299
|
+
errorType: "expired",
|
|
1300
|
+
afterRetry: isRetry
|
|
1301
|
+
});
|
|
1302
|
+
} else if (response.status === 403) {
|
|
1303
|
+
this.config.onAuthError({
|
|
1304
|
+
status: response.status,
|
|
1305
|
+
message: responseData.message || "Forbidden - insufficient permissions or missing scope",
|
|
1306
|
+
response: responseData,
|
|
1307
|
+
errorType: "forbidden",
|
|
1308
|
+
afterRetry: isRetry
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
423
1311
|
}
|
|
424
1312
|
const errorMessage = responseData.message || responseData.error || responseData.detail || (typeof responseData === "string" ? responseData : `API request failed: ${response.status}`);
|
|
425
1313
|
throw new RemoteDBError(errorMessage, response.status, responseData);
|
|
@@ -618,22 +1506,824 @@ var RemoteDB = class {
|
|
|
618
1506
|
}
|
|
619
1507
|
};
|
|
620
1508
|
|
|
621
|
-
// src/
|
|
622
|
-
|
|
1509
|
+
// src/core/auth/AuthManager.ts
|
|
1510
|
+
import { jwtDecode } from "jwt-decode";
|
|
623
1511
|
|
|
624
|
-
//
|
|
625
|
-
var
|
|
1512
|
+
// src/utils/storage.ts
|
|
1513
|
+
var LocalStorageAdapter = class {
|
|
1514
|
+
async get(key) {
|
|
1515
|
+
return localStorage.getItem(key);
|
|
1516
|
+
}
|
|
1517
|
+
async set(key, value) {
|
|
1518
|
+
localStorage.setItem(key, value);
|
|
1519
|
+
}
|
|
1520
|
+
async remove(key) {
|
|
1521
|
+
localStorage.removeItem(key);
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
var STORAGE_KEYS = {
|
|
1525
|
+
REFRESH_TOKEN: "basic_refresh_token",
|
|
1526
|
+
USER_INFO: "basic_user_info",
|
|
1527
|
+
AUTH_STATE: "basic_auth_state",
|
|
1528
|
+
REDIRECT_URI: "basic_redirect_uri",
|
|
1529
|
+
SERVER_URL: "basic_server_url",
|
|
1530
|
+
PDS_ENDPOINTS: "basic_pds_endpoints",
|
|
1531
|
+
LAST_CONNECT_REPORT: "basic_last_connect_report",
|
|
1532
|
+
DEBUG: "basic_debug",
|
|
1533
|
+
CODE_VERIFIER: "basic_code_verifier"
|
|
1534
|
+
};
|
|
626
1535
|
|
|
627
|
-
// src/
|
|
628
|
-
var
|
|
1536
|
+
// src/utils/normalizeClientId.ts
|
|
1537
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1538
|
+
function normalizeClientId(projectId, adminHostname = "api.basic.tech") {
|
|
1539
|
+
if (!projectId) return projectId;
|
|
1540
|
+
if (projectId === "self") return projectId;
|
|
1541
|
+
if (projectId.startsWith("did:")) return projectId;
|
|
1542
|
+
if (UUID_RE.test(projectId)) {
|
|
1543
|
+
const hex = projectId.replace(/-/g, "").toLowerCase();
|
|
1544
|
+
return `did:web:${adminHostname}:projects:${hex}`;
|
|
1545
|
+
}
|
|
1546
|
+
return projectId;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// src/utils/resolveDid.ts
|
|
1550
|
+
function resolveDidWebUrl(did) {
|
|
1551
|
+
if (!did.startsWith("did:web:")) return null;
|
|
1552
|
+
const rest = did.slice(8);
|
|
1553
|
+
if (!rest) return null;
|
|
1554
|
+
const parts = rest.split(":");
|
|
1555
|
+
const hostname = parts[0].replace(/%3A/gi, ":");
|
|
1556
|
+
if (parts.length === 1) {
|
|
1557
|
+
return `https://${hostname}/.well-known/did.json`;
|
|
1558
|
+
}
|
|
1559
|
+
const pathParts = parts.slice(1).map((p) => decodeURIComponent(p));
|
|
1560
|
+
return `https://${hostname}/${pathParts.join("/")}/did.json`;
|
|
1561
|
+
}
|
|
1562
|
+
async function resolveFromDocument(did, didDocument) {
|
|
1563
|
+
const services = didDocument.service;
|
|
1564
|
+
const pdsService = services?.find(
|
|
1565
|
+
(s) => s.id === "#basic_pds" || s.id === `${did}#basic_pds`
|
|
1566
|
+
);
|
|
1567
|
+
if (!pdsService) {
|
|
1568
|
+
throw new Error(`DID document has no #basic_pds service entry`);
|
|
1569
|
+
}
|
|
1570
|
+
const pdsUrl = pdsService.serviceEndpoint.replace(/\/+$/, "");
|
|
1571
|
+
const oauthRes = await fetch(`${pdsUrl}/auth/.well-known/openid-configuration`);
|
|
1572
|
+
if (!oauthRes.ok) {
|
|
1573
|
+
throw new Error(`Failed to fetch OpenID configuration from ${pdsUrl}: ${oauthRes.status}`);
|
|
1574
|
+
}
|
|
1575
|
+
const oauth = await oauthRes.json();
|
|
1576
|
+
return {
|
|
1577
|
+
did,
|
|
1578
|
+
didDocument,
|
|
1579
|
+
pdsUrl,
|
|
1580
|
+
authorization_endpoint: oauth.authorization_endpoint,
|
|
1581
|
+
token_endpoint: oauth.token_endpoint,
|
|
1582
|
+
userinfo_endpoint: oauth.userinfo_endpoint
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
async function resolveDid(did) {
|
|
1586
|
+
const url = resolveDidWebUrl(did);
|
|
1587
|
+
if (!url) {
|
|
1588
|
+
throw new Error(`Unsupported DID method: ${did}`);
|
|
1589
|
+
}
|
|
1590
|
+
const didRes = await fetch(url);
|
|
1591
|
+
if (!didRes.ok) {
|
|
1592
|
+
throw new Error(`Failed to fetch DID document at ${url}: ${didRes.status}`);
|
|
1593
|
+
}
|
|
1594
|
+
const didDocument = await didRes.json();
|
|
1595
|
+
return resolveFromDocument(did, didDocument);
|
|
1596
|
+
}
|
|
1597
|
+
async function resolveHandle(handle) {
|
|
1598
|
+
const res = await fetch(`https://${handle}/.well-known/did.json`);
|
|
1599
|
+
if (!res.ok) {
|
|
1600
|
+
throw new Error(`Handle resolution failed for ${handle}: ${res.status}`);
|
|
1601
|
+
}
|
|
1602
|
+
const didDocument = await res.json();
|
|
1603
|
+
const did = didDocument.id;
|
|
1604
|
+
if (!did) {
|
|
1605
|
+
throw new Error(`Handle response has no 'id' field`);
|
|
1606
|
+
}
|
|
1607
|
+
const resolved = await resolveFromDocument(did, didDocument);
|
|
1608
|
+
resolved.handle = handle;
|
|
1609
|
+
return resolved;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// src/core/auth/AuthManager.ts
|
|
1613
|
+
init_network();
|
|
1614
|
+
init_config();
|
|
1615
|
+
function generateCodeVerifier() {
|
|
1616
|
+
const array = new Uint8Array(32);
|
|
1617
|
+
crypto.getRandomValues(array);
|
|
1618
|
+
return base64UrlEncode(array);
|
|
1619
|
+
}
|
|
1620
|
+
async function generateCodeChallenge(verifier) {
|
|
1621
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
1622
|
+
log("crypto.subtle unavailable (non-secure context?) -- falling back to plain PKCE challenge");
|
|
1623
|
+
return { challenge: verifier, method: "plain" };
|
|
1624
|
+
}
|
|
1625
|
+
const encoder = new TextEncoder();
|
|
1626
|
+
const data = encoder.encode(verifier);
|
|
1627
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
1628
|
+
return { challenge: base64UrlEncode(new Uint8Array(digest)), method: "S256" };
|
|
1629
|
+
}
|
|
1630
|
+
function base64UrlEncode(buffer) {
|
|
1631
|
+
let str = "";
|
|
1632
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
1633
|
+
str += String.fromCharCode(buffer[i]);
|
|
1634
|
+
}
|
|
1635
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1636
|
+
}
|
|
1637
|
+
var AuthManager = class {
|
|
1638
|
+
// --- Public state (read by the UI layer) ---
|
|
1639
|
+
token = null;
|
|
1640
|
+
user = null;
|
|
1641
|
+
isSignedIn = false;
|
|
1642
|
+
isAuthReady = false;
|
|
1643
|
+
did = null;
|
|
1644
|
+
/** Space-separated scopes granted in the current access token */
|
|
1645
|
+
tokenScope = null;
|
|
1646
|
+
/** Space-separated scopes originally requested in the auth config */
|
|
1647
|
+
requestedScopes;
|
|
1648
|
+
config;
|
|
629
1649
|
storage;
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1650
|
+
/** True only during a user-initiated OAuth code exchange (not session restore) */
|
|
1651
|
+
freshSignIn = false;
|
|
1652
|
+
// --- Private ---
|
|
1653
|
+
notify;
|
|
1654
|
+
refreshPromise = null;
|
|
1655
|
+
codeExchangePromise = null;
|
|
1656
|
+
pendingRefresh = false;
|
|
1657
|
+
isOnline = typeof navigator !== "undefined" ? navigator.onLine : true;
|
|
1658
|
+
channel = null;
|
|
1659
|
+
constructor(config, storage, notify) {
|
|
1660
|
+
this.config = config;
|
|
634
1661
|
this.storage = storage;
|
|
635
|
-
this.
|
|
636
|
-
this.
|
|
1662
|
+
this.notify = notify;
|
|
1663
|
+
this.requestedScopes = config.scopes;
|
|
1664
|
+
this.initCrossTabSync();
|
|
1665
|
+
}
|
|
1666
|
+
initCrossTabSync() {
|
|
1667
|
+
if (typeof BroadcastChannel === "undefined") return;
|
|
1668
|
+
try {
|
|
1669
|
+
this.channel = new BroadcastChannel("basic-auth");
|
|
1670
|
+
this.channel.onmessage = (event) => {
|
|
1671
|
+
if (event.data?.type === "token_refreshed") {
|
|
1672
|
+
log("Received token refresh from another tab");
|
|
1673
|
+
if (event.data.accessToken && this.token) {
|
|
1674
|
+
this.token = { ...this.token, access_token: event.data.accessToken };
|
|
1675
|
+
}
|
|
1676
|
+
if (event.data.did) this.did = event.data.did;
|
|
1677
|
+
if (event.data.tokenScope) this.tokenScope = event.data.tokenScope;
|
|
1678
|
+
this.notify();
|
|
1679
|
+
}
|
|
1680
|
+
if (event.data?.type === "signed_in") {
|
|
1681
|
+
log("Received sign-in from another tab, reloading");
|
|
1682
|
+
if (typeof window !== "undefined") {
|
|
1683
|
+
window.location.reload();
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
if (event.data?.type === "signed_out") {
|
|
1687
|
+
log("Received sign-out from another tab, reloading");
|
|
1688
|
+
this.user = null;
|
|
1689
|
+
this.isSignedIn = false;
|
|
1690
|
+
this.token = null;
|
|
1691
|
+
this.did = null;
|
|
1692
|
+
this.tokenScope = null;
|
|
1693
|
+
this.notify();
|
|
1694
|
+
if (typeof window !== "undefined") {
|
|
1695
|
+
window.location.reload();
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
} catch {
|
|
1700
|
+
log("BroadcastChannel not available for cross-tab sync");
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
broadcastTokenRefresh() {
|
|
1704
|
+
this.channel?.postMessage({
|
|
1705
|
+
type: "token_refreshed",
|
|
1706
|
+
accessToken: this.token?.access_token,
|
|
1707
|
+
did: this.did,
|
|
1708
|
+
tokenScope: this.tokenScope
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
broadcastSignIn() {
|
|
1712
|
+
this.channel?.postMessage({ type: "signed_in" });
|
|
1713
|
+
}
|
|
1714
|
+
broadcastSignOut() {
|
|
1715
|
+
this.channel?.postMessage({ type: "signed_out" });
|
|
1716
|
+
}
|
|
1717
|
+
// ------------------------------------------------------------------
|
|
1718
|
+
// Public API
|
|
1719
|
+
// ------------------------------------------------------------------
|
|
1720
|
+
/**
|
|
1721
|
+
* Bootstrap auth: handle OAuth callback (?code=), restore session
|
|
1722
|
+
* from refresh token, or load cached user for offline mode.
|
|
1723
|
+
*/
|
|
1724
|
+
async initialize() {
|
|
1725
|
+
await this.storage.set(STORAGE_KEYS.DEBUG, this.config.debug ? "true" : "false");
|
|
1726
|
+
const storedServerUrl = await this.storage.get(STORAGE_KEYS.SERVER_URL);
|
|
1727
|
+
if (storedServerUrl && storedServerUrl !== this.config.pdsUrl) {
|
|
1728
|
+
log("PDS URL changed, clearing stored tokens");
|
|
1729
|
+
await this.clearStoredAuth();
|
|
1730
|
+
}
|
|
1731
|
+
await this.storage.set(STORAGE_KEYS.SERVER_URL, this.config.pdsUrl);
|
|
1732
|
+
try {
|
|
1733
|
+
const params = new URLSearchParams(window.location.search);
|
|
1734
|
+
if (params.has("code")) {
|
|
1735
|
+
const code = params.get("code");
|
|
1736
|
+
if (!code) {
|
|
1737
|
+
this.isAuthReady = true;
|
|
1738
|
+
this.notify();
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
const state = await this.storage.get(STORAGE_KEYS.AUTH_STATE);
|
|
1742
|
+
const urlState = params.get("state");
|
|
1743
|
+
if (!state || state !== urlState) {
|
|
1744
|
+
log("error: auth state does not match");
|
|
1745
|
+
this.isAuthReady = true;
|
|
1746
|
+
this.notify();
|
|
1747
|
+
await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
|
|
1748
|
+
cleanOAuthParamsFromUrl();
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
|
|
1752
|
+
cleanOAuthParamsFromUrl();
|
|
1753
|
+
this.freshSignIn = true;
|
|
1754
|
+
this.exchangeToken(code, false).catch((error) => {
|
|
1755
|
+
log("Error fetching token:", error);
|
|
1756
|
+
});
|
|
1757
|
+
} else {
|
|
1758
|
+
const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1759
|
+
if (refreshToken) {
|
|
1760
|
+
log("Found refresh token in storage, attempting to refresh access token");
|
|
1761
|
+
this.exchangeToken(refreshToken, true).catch((error) => {
|
|
1762
|
+
log("Error fetching refresh token:", error);
|
|
1763
|
+
});
|
|
1764
|
+
} else {
|
|
1765
|
+
const cachedUserInfo = await this.storage.get(STORAGE_KEYS.USER_INFO);
|
|
1766
|
+
if (cachedUserInfo) {
|
|
1767
|
+
try {
|
|
1768
|
+
this.user = JSON.parse(cachedUserInfo);
|
|
1769
|
+
this.isSignedIn = true;
|
|
1770
|
+
log("Loaded cached user info for offline mode");
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
log("Error parsing cached user info:", error);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
this.isAuthReady = true;
|
|
1776
|
+
this.notify();
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
} catch (e) {
|
|
1780
|
+
log("error getting token", e);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Get a valid access token string. Refreshes proactively (5s buffer)
|
|
1785
|
+
* or on demand (forceRefresh). Mutex prevents concurrent refreshes.
|
|
1786
|
+
*/
|
|
1787
|
+
async getToken(options) {
|
|
1788
|
+
log("getting token...");
|
|
1789
|
+
if (!this.token) {
|
|
1790
|
+
const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1791
|
+
if (refreshToken) {
|
|
1792
|
+
log("No token in memory, attempting to refresh from storage");
|
|
1793
|
+
if (this.refreshPromise) {
|
|
1794
|
+
log("Token refresh already in progress, waiting...");
|
|
1795
|
+
try {
|
|
1796
|
+
const newToken = await this.refreshPromise;
|
|
1797
|
+
if (newToken?.access_token) {
|
|
1798
|
+
return newToken.access_token;
|
|
1799
|
+
}
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
log("In-flight refresh failed:", error);
|
|
1802
|
+
throw error;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
try {
|
|
1806
|
+
const newToken = await this.exchangeToken(refreshToken, true);
|
|
1807
|
+
if (newToken?.access_token) {
|
|
1808
|
+
return newToken.access_token;
|
|
1809
|
+
}
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
log("Failed to refresh token from storage:", error);
|
|
1812
|
+
if (this.isNetworkError(error)) {
|
|
1813
|
+
throw new Error("Network offline - authentication will be retried when online");
|
|
1814
|
+
}
|
|
1815
|
+
throw new Error("Authentication expired. Please sign in again.");
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
log("no token found");
|
|
1819
|
+
throw new Error("no token found");
|
|
1820
|
+
}
|
|
1821
|
+
const decoded = jwtDecode(this.token.access_token);
|
|
1822
|
+
const expirationBuffer = 5;
|
|
1823
|
+
const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
|
|
1824
|
+
const shouldRefresh = isExpired || options?.forceRefresh === true;
|
|
1825
|
+
if (shouldRefresh) {
|
|
1826
|
+
log(options?.forceRefresh ? "force refreshing token..." : "token is expired - refreshing ...");
|
|
1827
|
+
if (this.refreshPromise) {
|
|
1828
|
+
log("Token refresh already in progress, waiting...");
|
|
1829
|
+
try {
|
|
1830
|
+
const newToken = await this.refreshPromise;
|
|
1831
|
+
return newToken?.access_token || "";
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
log("In-flight refresh failed:", error);
|
|
1834
|
+
if (this.isNetworkError(error)) {
|
|
1835
|
+
log("Network issue - using expired token until network is restored");
|
|
1836
|
+
return this.token.access_token;
|
|
1837
|
+
}
|
|
1838
|
+
throw error;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
const refreshToken = this.token.refresh_token || await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1842
|
+
if (refreshToken) {
|
|
1843
|
+
try {
|
|
1844
|
+
const newToken = await this.exchangeToken(refreshToken, true);
|
|
1845
|
+
return newToken?.access_token || "";
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
log("Failed to refresh expired token:", error);
|
|
1848
|
+
if (this.isNetworkError(error)) {
|
|
1849
|
+
log("Network issue - using expired token until network is restored");
|
|
1850
|
+
return this.token.access_token;
|
|
1851
|
+
}
|
|
1852
|
+
throw new Error("Authentication expired. Please sign in again.");
|
|
1853
|
+
}
|
|
1854
|
+
} else {
|
|
1855
|
+
throw new Error("no refresh token available");
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
return this.token.access_token || "";
|
|
1859
|
+
}
|
|
1860
|
+
async getSignInUrl(redirectUri, endpoints) {
|
|
1861
|
+
log("getting sign in link...");
|
|
1862
|
+
if (!this.config.projectId) {
|
|
1863
|
+
throw new Error("Project ID is required to generate sign-in link");
|
|
1864
|
+
}
|
|
1865
|
+
const pdsEndpoints = endpoints || this.defaultPdsEndpoints();
|
|
1866
|
+
await this.storage.set(STORAGE_KEYS.PDS_ENDPOINTS, JSON.stringify(pdsEndpoints));
|
|
1867
|
+
const randomState = Math.random().toString(36).substring(6);
|
|
1868
|
+
await this.storage.set(STORAGE_KEYS.AUTH_STATE, randomState);
|
|
1869
|
+
const redirectUrl = redirectUri || window.location.href;
|
|
1870
|
+
if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
|
|
1871
|
+
throw new Error("Invalid redirect URI provided");
|
|
1872
|
+
}
|
|
1873
|
+
await this.storage.set(STORAGE_KEYS.REDIRECT_URI, redirectUrl);
|
|
1874
|
+
log("Stored redirect_uri for token exchange:", redirectUrl);
|
|
1875
|
+
const codeVerifier = generateCodeVerifier();
|
|
1876
|
+
const { challenge: codeChallenge, method: challengeMethod } = await generateCodeChallenge(codeVerifier);
|
|
1877
|
+
await this.storage.set(STORAGE_KEYS.CODE_VERIFIER, codeVerifier);
|
|
1878
|
+
let baseUrl = pdsEndpoints.authorization_endpoint;
|
|
1879
|
+
baseUrl += `?client_id=${encodeURIComponent(normalizeClientId(this.config.projectId, this.adminHostname))}`;
|
|
1880
|
+
baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`;
|
|
1881
|
+
baseUrl += `&response_type=code`;
|
|
1882
|
+
baseUrl += `&scope=${encodeURIComponent(this.config.scopes)}`;
|
|
1883
|
+
baseUrl += `&state=${randomState}`;
|
|
1884
|
+
baseUrl += `&code_challenge=${encodeURIComponent(codeChallenge)}`;
|
|
1885
|
+
baseUrl += `&code_challenge_method=${challengeMethod}`;
|
|
1886
|
+
log("Generated sign-in link successfully with scopes:", this.config.scopes);
|
|
1887
|
+
return baseUrl;
|
|
1888
|
+
}
|
|
1889
|
+
async signIn(redirectUri) {
|
|
1890
|
+
log("signing in...");
|
|
1891
|
+
if (!this.config.projectId) {
|
|
1892
|
+
log("Error: project_id is required for sign-in");
|
|
1893
|
+
throw new Error("Project ID is required for authentication");
|
|
1894
|
+
}
|
|
1895
|
+
const signInLink = await this.getSignInUrl(redirectUri);
|
|
1896
|
+
log("Generated sign-in link:", signInLink);
|
|
1897
|
+
try {
|
|
1898
|
+
new URL(signInLink);
|
|
1899
|
+
} catch {
|
|
1900
|
+
log("Error: Invalid sign-in link generated");
|
|
1901
|
+
throw new Error("Failed to generate valid sign-in URL");
|
|
1902
|
+
}
|
|
1903
|
+
window.location.href = signInLink;
|
|
1904
|
+
}
|
|
1905
|
+
async signInWithHandle(handle) {
|
|
1906
|
+
log("signing in with handle:", handle);
|
|
1907
|
+
if (!this.config.projectId) {
|
|
1908
|
+
throw new Error("Project ID is required for authentication");
|
|
1909
|
+
}
|
|
1910
|
+
const resolved = await resolveHandle(handle);
|
|
1911
|
+
log("Resolved handle to PDS:", resolved.pdsUrl);
|
|
1912
|
+
const endpoints = {
|
|
1913
|
+
pds_url: resolved.pdsUrl,
|
|
1914
|
+
authorization_endpoint: resolved.authorization_endpoint,
|
|
1915
|
+
token_endpoint: resolved.token_endpoint,
|
|
1916
|
+
userinfo_endpoint: resolved.userinfo_endpoint
|
|
1917
|
+
};
|
|
1918
|
+
const signInLink = await this.getSignInUrl(void 0, endpoints);
|
|
1919
|
+
log("Generated federated sign-in link:", signInLink);
|
|
1920
|
+
try {
|
|
1921
|
+
new URL(signInLink);
|
|
1922
|
+
} catch {
|
|
1923
|
+
throw new Error("Failed to generate valid sign-in URL");
|
|
1924
|
+
}
|
|
1925
|
+
window.location.href = signInLink;
|
|
1926
|
+
}
|
|
1927
|
+
async signInWithCode(code, state) {
|
|
1928
|
+
try {
|
|
1929
|
+
log("signInWithCode called with code:", code);
|
|
1930
|
+
if (!code || typeof code !== "string") {
|
|
1931
|
+
return { success: false, error: "Invalid authorization code" };
|
|
1932
|
+
}
|
|
1933
|
+
if (state) {
|
|
1934
|
+
const storedState = await this.storage.get(STORAGE_KEYS.AUTH_STATE);
|
|
1935
|
+
if (storedState && storedState !== state) {
|
|
1936
|
+
log("State parameter mismatch:", { provided: state, stored: storedState });
|
|
1937
|
+
return { success: false, error: "State parameter mismatch" };
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
|
|
1941
|
+
cleanOAuthParamsFromUrl();
|
|
1942
|
+
this.freshSignIn = true;
|
|
1943
|
+
const token = await this.exchangeToken(code, false);
|
|
1944
|
+
if (token) {
|
|
1945
|
+
log("signInWithCode successful");
|
|
1946
|
+
return { success: true };
|
|
1947
|
+
} else {
|
|
1948
|
+
return { success: false, error: "Failed to exchange code for token" };
|
|
1949
|
+
}
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
log("signInWithCode error:", error);
|
|
1952
|
+
return {
|
|
1953
|
+
success: false,
|
|
1954
|
+
error: error.message || "Authentication failed"
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Clear auth state and storage. Does NOT handle sync/DB cleanup —
|
|
1960
|
+
* the UI layer (BasicProvider) wraps this to add sync teardown.
|
|
1961
|
+
*/
|
|
1962
|
+
async signOut() {
|
|
1963
|
+
log("signing out!");
|
|
1964
|
+
this.resetAuthState();
|
|
1965
|
+
await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
|
|
1966
|
+
await this.storage.remove(STORAGE_KEYS.LAST_CONNECT_REPORT);
|
|
1967
|
+
await this.clearStoredAuth();
|
|
1968
|
+
this.broadcastSignOut();
|
|
1969
|
+
this.notify();
|
|
1970
|
+
}
|
|
1971
|
+
hasScope(scope) {
|
|
1972
|
+
if (!this.tokenScope) return false;
|
|
1973
|
+
return this.tokenScope.split(/[\s,]+/).filter(Boolean).includes(scope);
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Returns scopes that were requested but not granted in the current token.
|
|
1977
|
+
* Useful after login or when a 403 is returned.
|
|
1978
|
+
*/
|
|
1979
|
+
missingScopes() {
|
|
1980
|
+
const requested = this.requestedScopes.split(/[\s,]+/).filter(Boolean);
|
|
1981
|
+
if (!this.tokenScope) return requested;
|
|
1982
|
+
const granted = new Set(this.tokenScope.split(/[\s,]+/).filter(Boolean));
|
|
1983
|
+
return requested.filter((s) => !granted.has(s));
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Register online/offline handlers that retry pending refreshes.
|
|
1987
|
+
* Returns a cleanup function for useEffect teardown.
|
|
1988
|
+
*/
|
|
1989
|
+
setupNetworkListeners() {
|
|
1990
|
+
const handleOnline = async () => {
|
|
1991
|
+
log("Network came back online");
|
|
1992
|
+
this.isOnline = true;
|
|
1993
|
+
if (this.pendingRefresh && this.token) {
|
|
1994
|
+
log("Retrying pending token refresh");
|
|
1995
|
+
this.pendingRefresh = false;
|
|
1996
|
+
const refreshToken = this.token.refresh_token || await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1997
|
+
if (refreshToken) {
|
|
1998
|
+
this.exchangeToken(refreshToken, true).catch((error) => {
|
|
1999
|
+
log("Retry refresh failed:", error);
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
const handleOffline = () => {
|
|
2005
|
+
log("Network went offline");
|
|
2006
|
+
this.isOnline = false;
|
|
2007
|
+
};
|
|
2008
|
+
window.addEventListener("online", handleOnline);
|
|
2009
|
+
window.addEventListener("offline", handleOffline);
|
|
2010
|
+
return () => {
|
|
2011
|
+
window.removeEventListener("online", handleOnline);
|
|
2012
|
+
window.removeEventListener("offline", handleOffline);
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
// ------------------------------------------------------------------
|
|
2016
|
+
// Private
|
|
2017
|
+
// ------------------------------------------------------------------
|
|
2018
|
+
get adminHostname() {
|
|
2019
|
+
try {
|
|
2020
|
+
return new URL(this.config.adminUrl).hostname;
|
|
2021
|
+
} catch {
|
|
2022
|
+
return "api.basic.tech";
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
defaultPdsEndpoints() {
|
|
2026
|
+
return {
|
|
2027
|
+
pds_url: this.config.pdsUrl,
|
|
2028
|
+
authorization_endpoint: `${this.config.pdsUrl}/auth/authorize`,
|
|
2029
|
+
token_endpoint: `${this.config.pdsUrl}/auth/token`,
|
|
2030
|
+
userinfo_endpoint: `${this.config.pdsUrl}/auth/userinfo`
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
async getActivePdsEndpoints() {
|
|
2034
|
+
const stored = await this.storage.get(STORAGE_KEYS.PDS_ENDPOINTS);
|
|
2035
|
+
if (stored) {
|
|
2036
|
+
try {
|
|
2037
|
+
return JSON.parse(stored);
|
|
2038
|
+
} catch {
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
return this.defaultPdsEndpoints();
|
|
2042
|
+
}
|
|
2043
|
+
async reportConnection(accessToken) {
|
|
2044
|
+
if (!this.config.projectId || !this.config.adminUrl) return;
|
|
2045
|
+
const lastReport = await this.storage.get(STORAGE_KEYS.LAST_CONNECT_REPORT);
|
|
2046
|
+
if (lastReport) {
|
|
2047
|
+
const elapsed = Date.now() - parseInt(lastReport, 10);
|
|
2048
|
+
if (elapsed < 24 * 60 * 60 * 1e3) return;
|
|
2049
|
+
}
|
|
2050
|
+
try {
|
|
2051
|
+
await fetch(`${this.config.adminUrl}/project/${this.config.projectId}/user/connect`, {
|
|
2052
|
+
method: "POST",
|
|
2053
|
+
headers: { "Content-Type": "application/json" },
|
|
2054
|
+
body: JSON.stringify({ token: accessToken })
|
|
2055
|
+
});
|
|
2056
|
+
await this.storage.set(STORAGE_KEYS.LAST_CONNECT_REPORT, Date.now().toString());
|
|
2057
|
+
log("Reported connection to admin server");
|
|
2058
|
+
} catch (err) {
|
|
2059
|
+
log("Failed to report connection (non-blocking):", err);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* After a new token is stored, decode JWT claims and fetch user info.
|
|
2064
|
+
*/
|
|
2065
|
+
async processNewToken() {
|
|
2066
|
+
if (!this.token) {
|
|
2067
|
+
this.isAuthReady = true;
|
|
2068
|
+
this.notify();
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
try {
|
|
2072
|
+
const decoded = jwtDecode(this.token.access_token);
|
|
2073
|
+
if (decoded.sub) this.did = decoded.sub;
|
|
2074
|
+
if (decoded.scope) this.tokenScope = decoded.scope;
|
|
2075
|
+
const expirationBuffer = 5;
|
|
2076
|
+
const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
|
|
2077
|
+
if (isExpired) {
|
|
2078
|
+
log("token is expired - refreshing ...");
|
|
2079
|
+
const refreshToken = this.token.refresh_token;
|
|
2080
|
+
if (!refreshToken) {
|
|
2081
|
+
log("Error: No refresh token available for expired token");
|
|
2082
|
+
this.isAuthReady = true;
|
|
2083
|
+
this.notify();
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
try {
|
|
2087
|
+
const newToken = await this.exchangeToken(refreshToken, true);
|
|
2088
|
+
await this.fetchUser(newToken?.access_token || "");
|
|
2089
|
+
} catch (error) {
|
|
2090
|
+
log("Failed to refresh token in processNewToken:", error);
|
|
2091
|
+
if (this.isNetworkError(error)) {
|
|
2092
|
+
log("Network issue - continuing with expired token until online");
|
|
2093
|
+
await this.fetchUser(this.token.access_token);
|
|
2094
|
+
} else {
|
|
2095
|
+
this.isAuthReady = true;
|
|
2096
|
+
this.notify();
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
} else {
|
|
2100
|
+
await this.fetchUser(this.token.access_token);
|
|
2101
|
+
}
|
|
2102
|
+
} catch (error) {
|
|
2103
|
+
log("Error processing token:", error);
|
|
2104
|
+
this.isAuthReady = true;
|
|
2105
|
+
this.notify();
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
async fetchUser(accessToken) {
|
|
2109
|
+
log("fetching user");
|
|
2110
|
+
try {
|
|
2111
|
+
const endpoints = await this.getActivePdsEndpoints();
|
|
2112
|
+
const response = await fetch(endpoints.userinfo_endpoint, {
|
|
2113
|
+
method: "GET",
|
|
2114
|
+
headers: { "Authorization": `Bearer ${accessToken}` }
|
|
2115
|
+
});
|
|
2116
|
+
if (!response.ok) {
|
|
2117
|
+
throw new Error(`Failed to fetch user info: ${response.status}`);
|
|
2118
|
+
}
|
|
2119
|
+
const user = await response.json();
|
|
2120
|
+
if (user.error) {
|
|
2121
|
+
log("error fetching user", user.error);
|
|
2122
|
+
throw new Error(`User info error: ${user.error}`);
|
|
2123
|
+
}
|
|
2124
|
+
if (this.token?.refresh_token) {
|
|
2125
|
+
await this.storage.set(STORAGE_KEYS.REFRESH_TOKEN, this.token.refresh_token);
|
|
2126
|
+
}
|
|
2127
|
+
await this.storage.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user));
|
|
2128
|
+
log("Cached user info in storage");
|
|
2129
|
+
this.user = user;
|
|
2130
|
+
this.isSignedIn = true;
|
|
2131
|
+
this.isAuthReady = true;
|
|
2132
|
+
if (this.freshSignIn) {
|
|
2133
|
+
this.freshSignIn = false;
|
|
2134
|
+
this.broadcastSignIn();
|
|
2135
|
+
} else {
|
|
2136
|
+
this.broadcastTokenRefresh();
|
|
2137
|
+
}
|
|
2138
|
+
this.notify();
|
|
2139
|
+
} catch (error) {
|
|
2140
|
+
log("Failed to fetch user info:", error);
|
|
2141
|
+
this.isAuthReady = true;
|
|
2142
|
+
this.notify();
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Exchange an auth code or refresh token for an access token.
|
|
2147
|
+
* Handles mutex (one in-flight refresh), token validation, and
|
|
2148
|
+
* triggers processNewToken on success.
|
|
2149
|
+
*/
|
|
2150
|
+
async exchangeToken(codeOrRefreshToken, isRefreshToken) {
|
|
2151
|
+
if (!codeOrRefreshToken || codeOrRefreshToken.trim() === "") {
|
|
2152
|
+
const errorMsg = isRefreshToken ? "Refresh token is empty or undefined" : "Authorization code is empty or undefined";
|
|
2153
|
+
log("Error:", errorMsg);
|
|
2154
|
+
throw new Error(errorMsg);
|
|
2155
|
+
}
|
|
2156
|
+
if (isRefreshToken && this.refreshPromise) {
|
|
2157
|
+
log("Reusing in-flight refresh token request");
|
|
2158
|
+
return this.refreshPromise;
|
|
2159
|
+
}
|
|
2160
|
+
if (!isRefreshToken && this.codeExchangePromise) {
|
|
2161
|
+
log("Reusing in-flight code exchange request");
|
|
2162
|
+
return this.codeExchangePromise;
|
|
2163
|
+
}
|
|
2164
|
+
const tokenPromise = (async () => {
|
|
2165
|
+
try {
|
|
2166
|
+
if (!this.isOnline) {
|
|
2167
|
+
log("Network is offline, marking refresh as pending");
|
|
2168
|
+
this.pendingRefresh = true;
|
|
2169
|
+
throw new Error("Network offline - refresh will be retried when online");
|
|
2170
|
+
}
|
|
2171
|
+
const endpoints = await this.getActivePdsEndpoints();
|
|
2172
|
+
let requestBody;
|
|
2173
|
+
if (isRefreshToken) {
|
|
2174
|
+
requestBody = {
|
|
2175
|
+
grant_type: "refresh_token",
|
|
2176
|
+
refresh_token: codeOrRefreshToken
|
|
2177
|
+
};
|
|
2178
|
+
if (this.config.projectId) {
|
|
2179
|
+
requestBody.client_id = normalizeClientId(this.config.projectId, this.adminHostname);
|
|
2180
|
+
}
|
|
2181
|
+
} else {
|
|
2182
|
+
requestBody = {
|
|
2183
|
+
grant_type: "authorization_code",
|
|
2184
|
+
code: codeOrRefreshToken
|
|
2185
|
+
};
|
|
2186
|
+
const storedRedirectUri = await this.storage.get(STORAGE_KEYS.REDIRECT_URI);
|
|
2187
|
+
if (storedRedirectUri) {
|
|
2188
|
+
requestBody.redirect_uri = storedRedirectUri;
|
|
2189
|
+
log("Including redirect_uri in token exchange:", storedRedirectUri);
|
|
2190
|
+
} else {
|
|
2191
|
+
log("Warning: No redirect_uri found in storage for token exchange");
|
|
2192
|
+
}
|
|
2193
|
+
const codeVerifier = await this.storage.get(STORAGE_KEYS.CODE_VERIFIER);
|
|
2194
|
+
if (codeVerifier) {
|
|
2195
|
+
requestBody.code_verifier = codeVerifier;
|
|
2196
|
+
}
|
|
2197
|
+
if (this.config.projectId) {
|
|
2198
|
+
requestBody.client_id = normalizeClientId(this.config.projectId, this.adminHostname);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
log("Token exchange request body:", {
|
|
2202
|
+
...requestBody,
|
|
2203
|
+
...isRefreshToken ? { refresh_token: "[REDACTED]" } : { code: "[REDACTED]" },
|
|
2204
|
+
...requestBody.code_verifier ? { code_verifier: "[REDACTED]" } : {}
|
|
2205
|
+
});
|
|
2206
|
+
const token = await fetch(endpoints.token_endpoint, {
|
|
2207
|
+
method: "POST",
|
|
2208
|
+
headers: { "Content-Type": "application/json" },
|
|
2209
|
+
body: JSON.stringify(requestBody)
|
|
2210
|
+
}).then((response) => response.json()).catch((error) => {
|
|
2211
|
+
log("Network error fetching token:", error);
|
|
2212
|
+
if (!this.isOnline) {
|
|
2213
|
+
this.pendingRefresh = true;
|
|
2214
|
+
throw new Error("Network offline - refresh will be retried when online");
|
|
2215
|
+
}
|
|
2216
|
+
throw new Error("Network error during token refresh");
|
|
2217
|
+
});
|
|
2218
|
+
if (token.access_token) {
|
|
2219
|
+
try {
|
|
2220
|
+
const decoded = jwtDecode(token.access_token);
|
|
2221
|
+
if (decoded.typ === "refresh") {
|
|
2222
|
+
log("Error: received refresh token as access token");
|
|
2223
|
+
throw new Error("Invalid token: received refresh token instead of access token");
|
|
2224
|
+
}
|
|
2225
|
+
} catch (decodeError) {
|
|
2226
|
+
if (decodeError.message.includes("Invalid token")) {
|
|
2227
|
+
throw decodeError;
|
|
2228
|
+
}
|
|
2229
|
+
log("Warning: could not decode access token for type check:", decodeError);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
if (token.error) {
|
|
2233
|
+
log("error fetching token", token.error);
|
|
2234
|
+
if (typeof token.error === "string" && (token.error.includes("network") || token.error.includes("timeout"))) {
|
|
2235
|
+
this.pendingRefresh = true;
|
|
2236
|
+
throw new Error("Network issue - refresh will be retried when online");
|
|
2237
|
+
}
|
|
2238
|
+
await this.clearStoredAuth();
|
|
2239
|
+
this.resetAuthState();
|
|
2240
|
+
this.notify();
|
|
2241
|
+
throw new Error(`Token refresh failed: ${token.error}`);
|
|
2242
|
+
} else {
|
|
2243
|
+
this.token = token;
|
|
2244
|
+
this.pendingRefresh = false;
|
|
2245
|
+
if (token.refresh_token) {
|
|
2246
|
+
await this.storage.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token);
|
|
2247
|
+
log("Updated refresh token in storage");
|
|
2248
|
+
}
|
|
2249
|
+
if (!isRefreshToken) {
|
|
2250
|
+
await this.storage.remove(STORAGE_KEYS.REDIRECT_URI);
|
|
2251
|
+
await this.storage.remove(STORAGE_KEYS.CODE_VERIFIER);
|
|
2252
|
+
log("Cleaned up redirect_uri and code_verifier from storage after successful exchange");
|
|
2253
|
+
}
|
|
2254
|
+
this.reportConnection(token.access_token).catch(() => {
|
|
2255
|
+
});
|
|
2256
|
+
await this.processNewToken();
|
|
2257
|
+
}
|
|
2258
|
+
return token;
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
log("Token refresh error:", error);
|
|
2261
|
+
if (!this.isNetworkError(error)) {
|
|
2262
|
+
await this.clearStoredAuth();
|
|
2263
|
+
this.resetAuthState();
|
|
2264
|
+
this.notify();
|
|
2265
|
+
}
|
|
2266
|
+
throw error;
|
|
2267
|
+
}
|
|
2268
|
+
})();
|
|
2269
|
+
if (isRefreshToken) {
|
|
2270
|
+
this.refreshPromise = tokenPromise;
|
|
2271
|
+
tokenPromise.finally(() => {
|
|
2272
|
+
if (this.refreshPromise === tokenPromise) {
|
|
2273
|
+
this.refreshPromise = null;
|
|
2274
|
+
log("Cleared refresh promise reference");
|
|
2275
|
+
}
|
|
2276
|
+
});
|
|
2277
|
+
} else {
|
|
2278
|
+
this.codeExchangePromise = tokenPromise;
|
|
2279
|
+
tokenPromise.finally(() => {
|
|
2280
|
+
if (this.codeExchangePromise === tokenPromise) {
|
|
2281
|
+
this.codeExchangePromise = null;
|
|
2282
|
+
log("Cleared code exchange promise reference");
|
|
2283
|
+
}
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
return tokenPromise;
|
|
2287
|
+
}
|
|
2288
|
+
resetAuthState() {
|
|
2289
|
+
this.user = null;
|
|
2290
|
+
this.isSignedIn = false;
|
|
2291
|
+
this.token = null;
|
|
2292
|
+
this.did = null;
|
|
2293
|
+
this.tokenScope = null;
|
|
2294
|
+
this.isAuthReady = true;
|
|
2295
|
+
}
|
|
2296
|
+
async clearStoredAuth() {
|
|
2297
|
+
await this.storage.remove(STORAGE_KEYS.REFRESH_TOKEN);
|
|
2298
|
+
await this.storage.remove(STORAGE_KEYS.USER_INFO);
|
|
2299
|
+
await this.storage.remove(STORAGE_KEYS.REDIRECT_URI);
|
|
2300
|
+
await this.storage.remove(STORAGE_KEYS.CODE_VERIFIER);
|
|
2301
|
+
await this.storage.remove(STORAGE_KEYS.SERVER_URL);
|
|
2302
|
+
await this.storage.remove(STORAGE_KEYS.PDS_ENDPOINTS);
|
|
2303
|
+
}
|
|
2304
|
+
isNetworkError(error) {
|
|
2305
|
+
if (error instanceof Error) {
|
|
2306
|
+
return error.message.includes("offline") || error.message.includes("Network");
|
|
2307
|
+
}
|
|
2308
|
+
return false;
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2311
|
+
|
|
2312
|
+
// src/AuthContext.tsx
|
|
2313
|
+
init_config();
|
|
2314
|
+
init_package();
|
|
2315
|
+
|
|
2316
|
+
// src/updater/versionUpdater.ts
|
|
2317
|
+
init_config();
|
|
2318
|
+
var VersionUpdater = class {
|
|
2319
|
+
storage;
|
|
2320
|
+
currentVersion;
|
|
2321
|
+
migrations;
|
|
2322
|
+
versionKey = "basic_app_version";
|
|
2323
|
+
constructor(storage, currentVersion, migrations = []) {
|
|
2324
|
+
this.storage = storage;
|
|
2325
|
+
this.currentVersion = currentVersion;
|
|
2326
|
+
this.migrations = migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
|
|
637
2327
|
}
|
|
638
2328
|
/**
|
|
639
2329
|
* Check current stored version and run migrations if needed
|
|
@@ -656,7 +2346,7 @@ var VersionUpdater = class {
|
|
|
656
2346
|
}
|
|
657
2347
|
for (const migration of migrationsToRun) {
|
|
658
2348
|
try {
|
|
659
|
-
|
|
2349
|
+
log(`Running migration from ${migration.fromVersion} to ${migration.toVersion}`);
|
|
660
2350
|
await migration.migrate(this.storage);
|
|
661
2351
|
} catch (error) {
|
|
662
2352
|
console.error(`Migration failed from ${migration.fromVersion} to ${migration.toVersion}:`, error);
|
|
@@ -669,8 +2359,7 @@ var VersionUpdater = class {
|
|
|
669
2359
|
async getStoredVersion() {
|
|
670
2360
|
try {
|
|
671
2361
|
const versionData = await this.storage.get(this.versionKey);
|
|
672
|
-
if (!versionData)
|
|
673
|
-
return null;
|
|
2362
|
+
if (!versionData) return null;
|
|
674
2363
|
const versionInfo = JSON.parse(versionData);
|
|
675
2364
|
return versionInfo.version;
|
|
676
2365
|
} catch (error) {
|
|
@@ -689,11 +2378,8 @@ var VersionUpdater = class {
|
|
|
689
2378
|
return this.migrations.filter((migration) => {
|
|
690
2379
|
const storedLessThanMigrationTo = this.compareVersions(fromVersion, migration.toVersion) < 0;
|
|
691
2380
|
const currentGreaterThanOrEqualMigrationTo = this.compareVersions(toVersion, migration.toVersion) >= 0;
|
|
692
|
-
console.log(`Checking migration ${migration.fromVersion} \u2192 ${migration.toVersion}:`);
|
|
693
|
-
console.log(` stored ${fromVersion} < migration.to ${migration.toVersion}: ${storedLessThanMigrationTo}`);
|
|
694
|
-
console.log(` current ${toVersion} >= migration.to ${migration.toVersion}: ${currentGreaterThanOrEqualMigrationTo}`);
|
|
695
2381
|
const shouldRun = storedLessThanMigrationTo && currentGreaterThanOrEqualMigrationTo;
|
|
696
|
-
|
|
2382
|
+
log(`Migration ${migration.fromVersion} \u2192 ${migration.toVersion}: shouldRun=${shouldRun}`);
|
|
697
2383
|
return shouldRun;
|
|
698
2384
|
});
|
|
699
2385
|
}
|
|
@@ -735,11 +2421,12 @@ function createVersionUpdater(storage, currentVersion, migrations = []) {
|
|
|
735
2421
|
}
|
|
736
2422
|
|
|
737
2423
|
// src/updater/updateMigrations.ts
|
|
2424
|
+
init_config();
|
|
738
2425
|
var addMigrationTimestamp = {
|
|
739
2426
|
fromVersion: "0.6.0",
|
|
740
2427
|
toVersion: "0.7.0",
|
|
741
2428
|
async migrate(storage) {
|
|
742
|
-
|
|
2429
|
+
log("Running migration 0.6.0 \u2192 0.7.0");
|
|
743
2430
|
storage.set("test_migration", "true");
|
|
744
2431
|
}
|
|
745
2432
|
};
|
|
@@ -749,129 +2436,15 @@ function getMigrations() {
|
|
|
749
2436
|
];
|
|
750
2437
|
}
|
|
751
2438
|
|
|
752
|
-
// src/
|
|
753
|
-
|
|
754
|
-
async get(key) {
|
|
755
|
-
return localStorage.getItem(key);
|
|
756
|
-
}
|
|
757
|
-
async set(key, value) {
|
|
758
|
-
localStorage.setItem(key, value);
|
|
759
|
-
}
|
|
760
|
-
async remove(key) {
|
|
761
|
-
localStorage.removeItem(key);
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
var STORAGE_KEYS = {
|
|
765
|
-
REFRESH_TOKEN: "basic_refresh_token",
|
|
766
|
-
USER_INFO: "basic_user_info",
|
|
767
|
-
AUTH_STATE: "basic_auth_state",
|
|
768
|
-
REDIRECT_URI: "basic_redirect_uri",
|
|
769
|
-
SERVER_URL: "basic_server_url",
|
|
770
|
-
DEBUG: "basic_debug"
|
|
771
|
-
};
|
|
772
|
-
function getCookie(name) {
|
|
773
|
-
let cookieValue = "";
|
|
774
|
-
if (document.cookie && document.cookie !== "") {
|
|
775
|
-
const cookies = document.cookie.split(";");
|
|
776
|
-
for (let i = 0; i < cookies.length; i++) {
|
|
777
|
-
const cookie = cookies[i]?.trim();
|
|
778
|
-
if (cookie && cookie.substring(0, name.length + 1) === name + "=") {
|
|
779
|
-
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
780
|
-
break;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
return cookieValue;
|
|
785
|
-
}
|
|
786
|
-
function setCookie(name, value, options) {
|
|
787
|
-
const opts = {
|
|
788
|
-
secure: true,
|
|
789
|
-
sameSite: "Strict",
|
|
790
|
-
httpOnly: false,
|
|
791
|
-
...options
|
|
792
|
-
};
|
|
793
|
-
let cookieString = `${name}=${value}`;
|
|
794
|
-
if (opts.secure)
|
|
795
|
-
cookieString += "; Secure";
|
|
796
|
-
if (opts.sameSite)
|
|
797
|
-
cookieString += `; SameSite=${opts.sameSite}`;
|
|
798
|
-
if (opts.httpOnly)
|
|
799
|
-
cookieString += "; HttpOnly";
|
|
800
|
-
document.cookie = cookieString;
|
|
801
|
-
}
|
|
802
|
-
function clearCookie(name) {
|
|
803
|
-
document.cookie = `${name}=; Secure; SameSite=Strict`;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// src/utils/network.ts
|
|
807
|
-
init_config();
|
|
808
|
-
function isDevelopment(debug) {
|
|
809
|
-
return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname.includes("localhost") || window.location.hostname.includes("127.0.0.1") || window.location.hostname.includes(".local") || process.env.NODE_ENV === "development" || debug === true;
|
|
810
|
-
}
|
|
811
|
-
async function checkForNewVersion() {
|
|
812
|
-
try {
|
|
813
|
-
const isBeta = version.includes("beta");
|
|
814
|
-
const response = await fetch(`https://registry.npmjs.org/@basictech/react/${isBeta ? "beta" : "latest"}`);
|
|
815
|
-
if (!response.ok) {
|
|
816
|
-
throw new Error("Failed to fetch version from npm");
|
|
817
|
-
}
|
|
818
|
-
const data = await response.json();
|
|
819
|
-
const latestVersion = data.version;
|
|
820
|
-
if (latestVersion !== version) {
|
|
821
|
-
console.warn("[basic] New version available:", latestVersion, `
|
|
822
|
-
run "npm install @basictech/react@${latestVersion}" to update`);
|
|
823
|
-
}
|
|
824
|
-
if (isBeta) {
|
|
825
|
-
log("thank you for being on basictech/react beta :)");
|
|
826
|
-
}
|
|
827
|
-
return {
|
|
828
|
-
hasNewVersion: version !== latestVersion,
|
|
829
|
-
latestVersion,
|
|
830
|
-
currentVersion: version
|
|
831
|
-
};
|
|
832
|
-
} catch (error) {
|
|
833
|
-
log("Error checking for new version:", error);
|
|
834
|
-
return {
|
|
835
|
-
hasNewVersion: false,
|
|
836
|
-
latestVersion: null,
|
|
837
|
-
currentVersion: null
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
function cleanOAuthParamsFromUrl() {
|
|
842
|
-
if (window.location.search.includes("code") || window.location.search.includes("state")) {
|
|
843
|
-
const url = new URL(window.location.href);
|
|
844
|
-
url.searchParams.delete("code");
|
|
845
|
-
url.searchParams.delete("state");
|
|
846
|
-
window.history.pushState({}, document.title, url.pathname + url.search);
|
|
847
|
-
log("Cleaned OAuth parameters from URL");
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
function getSyncStatus(statusCode) {
|
|
851
|
-
switch (statusCode) {
|
|
852
|
-
case -1:
|
|
853
|
-
return "ERROR";
|
|
854
|
-
case 0:
|
|
855
|
-
return "OFFLINE";
|
|
856
|
-
case 1:
|
|
857
|
-
return "CONNECTING";
|
|
858
|
-
case 2:
|
|
859
|
-
return "ONLINE";
|
|
860
|
-
case 3:
|
|
861
|
-
return "SYNCING";
|
|
862
|
-
case 4:
|
|
863
|
-
return "ERROR_WILL_RETRY";
|
|
864
|
-
default:
|
|
865
|
-
return "UNKNOWN";
|
|
866
|
-
}
|
|
867
|
-
}
|
|
2439
|
+
// src/AuthContext.tsx
|
|
2440
|
+
init_network();
|
|
868
2441
|
|
|
869
2442
|
// src/utils/schema.ts
|
|
870
2443
|
init_config();
|
|
871
|
-
import { validateSchema
|
|
2444
|
+
import { validateSchema, compareSchemas } from "@basictech/schema";
|
|
872
2445
|
async function getSchemaStatus(schema) {
|
|
873
2446
|
const projectId = schema.project_id;
|
|
874
|
-
const valid =
|
|
2447
|
+
const valid = validateSchema(schema);
|
|
875
2448
|
if (!valid.valid) {
|
|
876
2449
|
console.warn("BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled");
|
|
877
2450
|
return {
|
|
@@ -887,7 +2460,6 @@ async function getSchemaStatus(schema) {
|
|
|
887
2460
|
latest: null
|
|
888
2461
|
};
|
|
889
2462
|
});
|
|
890
|
-
console.log("latestSchema", latestSchema);
|
|
891
2463
|
if (!latestSchema.version) {
|
|
892
2464
|
return {
|
|
893
2465
|
valid: false,
|
|
@@ -934,7 +2506,7 @@ async function getSchemaStatus(schema) {
|
|
|
934
2506
|
}
|
|
935
2507
|
}
|
|
936
2508
|
async function validateAndCheckSchema(schema) {
|
|
937
|
-
const valid =
|
|
2509
|
+
const valid = validateSchema(schema);
|
|
938
2510
|
if (!valid.valid) {
|
|
939
2511
|
log("Basic Schema is invalid!", valid.errors);
|
|
940
2512
|
console.group("Schema Errors");
|
|
@@ -956,6 +2528,7 @@ async function validateAndCheckSchema(schema) {
|
|
|
956
2528
|
schemaStatus = await getSchemaStatus(schema);
|
|
957
2529
|
log("schemaStatus", schemaStatus);
|
|
958
2530
|
} else {
|
|
2531
|
+
schemaStatus = { valid: false, status: "unpublished" };
|
|
959
2532
|
log("schema not published - at version 0");
|
|
960
2533
|
}
|
|
961
2534
|
return {
|
|
@@ -965,40 +2538,28 @@ async function validateAndCheckSchema(schema) {
|
|
|
965
2538
|
}
|
|
966
2539
|
|
|
967
2540
|
// src/AuthContext.tsx
|
|
968
|
-
|
|
2541
|
+
init_context();
|
|
2542
|
+
init_context();
|
|
2543
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2544
|
+
var BasicDevToolbar2 = lazy(
|
|
2545
|
+
() => Promise.resolve().then(() => (init_BasicDevToolbar(), BasicDevToolbar_exports)).then((m) => ({ default: m.BasicDevToolbar }))
|
|
2546
|
+
);
|
|
969
2547
|
var DEFAULT_AUTH_CONFIG = {
|
|
970
2548
|
scopes: "profile,email,app:admin",
|
|
971
|
-
|
|
2549
|
+
pds_url: "https://pds.basic.id",
|
|
2550
|
+
admin_url: "https://api.basic.tech",
|
|
972
2551
|
ws_url: "wss://pds.basic.id/ws"
|
|
973
2552
|
};
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
// Auth actions
|
|
985
|
-
signIn: () => Promise.resolve(),
|
|
986
|
-
signOut: () => Promise.resolve(),
|
|
987
|
-
signInWithCode: () => Promise.resolve({ success: false }),
|
|
988
|
-
// Token management
|
|
989
|
-
getToken: () => Promise.reject(new Error("no token")),
|
|
990
|
-
getSignInUrl: () => Promise.resolve(""),
|
|
991
|
-
// DB access
|
|
992
|
-
db: noDb,
|
|
993
|
-
dbStatus: "LOADING" /* LOADING */,
|
|
994
|
-
dbMode: "sync",
|
|
995
|
-
// Legacy aliases
|
|
996
|
-
isAuthReady: false,
|
|
997
|
-
signin: () => Promise.resolve(),
|
|
998
|
-
signout: () => Promise.resolve(),
|
|
999
|
-
signinWithCode: () => Promise.resolve({ success: false }),
|
|
1000
|
-
getSignInLink: () => Promise.resolve("")
|
|
1001
|
-
});
|
|
2553
|
+
function snapshotAuth(mgr) {
|
|
2554
|
+
return {
|
|
2555
|
+
isSignedIn: mgr.isSignedIn,
|
|
2556
|
+
hasToken: !!mgr.token,
|
|
2557
|
+
isAuthReady: mgr.isAuthReady,
|
|
2558
|
+
user: mgr.user,
|
|
2559
|
+
did: mgr.did,
|
|
2560
|
+
tokenScope: mgr.tokenScope
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
1002
2563
|
function BasicProvider({
|
|
1003
2564
|
children,
|
|
1004
2565
|
project_id: project_id_prop,
|
|
@@ -1006,67 +2567,121 @@ function BasicProvider({
|
|
|
1006
2567
|
debug = false,
|
|
1007
2568
|
storage,
|
|
1008
2569
|
auth,
|
|
1009
|
-
dbMode = "sync"
|
|
2570
|
+
dbMode = "sync",
|
|
2571
|
+
devToolbar = false
|
|
1010
2572
|
}) {
|
|
1011
2573
|
const project_id = schema?.project_id || project_id_prop;
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
const [user, setUser] = useState({});
|
|
1016
|
-
const [shouldConnect, setShouldConnect] = useState(false);
|
|
1017
|
-
const [isReady, setIsReady] = useState(false);
|
|
1018
|
-
const [dbStatus, setDbStatus] = useState("OFFLINE" /* OFFLINE */);
|
|
1019
|
-
const [error, setError] = useState(null);
|
|
1020
|
-
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
|
1021
|
-
const [pendingRefresh, setPendingRefresh] = useState(false);
|
|
1022
|
-
const syncRef = useRef(null);
|
|
1023
|
-
const remoteDbRef = useRef(null);
|
|
1024
|
-
const storageAdapter = storage || new LocalStorageAdapter();
|
|
2574
|
+
if (auth?.server_url && !auth?.pds_url) {
|
|
2575
|
+
log("Warning: auth.server_url is deprecated, use auth.pds_url instead");
|
|
2576
|
+
}
|
|
1025
2577
|
const authConfig = {
|
|
1026
2578
|
scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
|
|
1027
|
-
|
|
2579
|
+
pds_url: auth?.pds_url || auth?.server_url || DEFAULT_AUTH_CONFIG.pds_url,
|
|
2580
|
+
admin_url: auth?.admin_url || DEFAULT_AUTH_CONFIG.admin_url,
|
|
1028
2581
|
ws_url: auth?.ws_url || DEFAULT_AUTH_CONFIG.ws_url
|
|
1029
2582
|
};
|
|
1030
2583
|
const scopesString = Array.isArray(authConfig.scopes) ? authConfig.scopes.join(" ") : authConfig.scopes;
|
|
1031
|
-
const
|
|
2584
|
+
const storageRef = useRef(storage || new LocalStorageAdapter());
|
|
2585
|
+
const storageAdapter = storageRef.current;
|
|
2586
|
+
const schemaRef = useRef(schema);
|
|
2587
|
+
schemaRef.current = schema;
|
|
2588
|
+
const [authState, setAuthState] = useState2({
|
|
2589
|
+
isSignedIn: false,
|
|
2590
|
+
hasToken: false,
|
|
2591
|
+
isAuthReady: false,
|
|
2592
|
+
user: null,
|
|
2593
|
+
did: null,
|
|
2594
|
+
tokenScope: null
|
|
2595
|
+
});
|
|
2596
|
+
const authRef = useRef(null);
|
|
2597
|
+
if (!authRef.current) {
|
|
2598
|
+
authRef.current = new AuthManager(
|
|
2599
|
+
{
|
|
2600
|
+
projectId: project_id,
|
|
2601
|
+
scopes: scopesString,
|
|
2602
|
+
pdsUrl: authConfig.pds_url,
|
|
2603
|
+
adminUrl: authConfig.admin_url,
|
|
2604
|
+
debug
|
|
2605
|
+
},
|
|
2606
|
+
storageAdapter,
|
|
2607
|
+
() => setAuthState(snapshotAuth(authRef.current))
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
const syncRef = useRef(null);
|
|
2611
|
+
const remoteDbRef = useRef(null);
|
|
2612
|
+
const [shouldConnect, setShouldConnect] = useState2(false);
|
|
2613
|
+
const [dbStatus, setDbStatus] = useState2("OFFLINE" /* OFFLINE */);
|
|
2614
|
+
const [isReady, setIsReady] = useState2(false);
|
|
2615
|
+
const [error, setError] = useState2(null);
|
|
2616
|
+
const [schemaDevInfo, setSchemaDevInfo] = useState2(null);
|
|
1032
2617
|
const isDevMode = () => isDevelopment(debug);
|
|
1033
|
-
const
|
|
2618
|
+
const refreshSchemaStatus = useCallback2(async () => {
|
|
2619
|
+
const s = schemaRef.current;
|
|
2620
|
+
if (!s) {
|
|
2621
|
+
setSchemaDevInfo(
|
|
2622
|
+
project_id ? {
|
|
2623
|
+
projectId: project_id,
|
|
2624
|
+
localVersion: void 0,
|
|
2625
|
+
status: "no_schema",
|
|
2626
|
+
valid: false,
|
|
2627
|
+
lastCheckedAt: Date.now()
|
|
2628
|
+
} : null
|
|
2629
|
+
);
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
const result = await validateAndCheckSchema(s);
|
|
2633
|
+
if (!result.isValid) {
|
|
2634
|
+
const errText = result.errors?.map((e) => e.message || "").join("; ") || "invalid";
|
|
2635
|
+
setSchemaDevInfo({
|
|
2636
|
+
projectId: s.project_id ?? null,
|
|
2637
|
+
localVersion: s.version,
|
|
2638
|
+
status: "invalid",
|
|
2639
|
+
valid: false,
|
|
2640
|
+
lastCheckedAt: Date.now(),
|
|
2641
|
+
error: errText
|
|
2642
|
+
});
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
setSchemaDevInfo({
|
|
2646
|
+
projectId: s.project_id ?? null,
|
|
2647
|
+
localVersion: s.version,
|
|
2648
|
+
status: result.schemaStatus.status ?? "unknown",
|
|
2649
|
+
valid: result.schemaStatus.valid,
|
|
2650
|
+
lastCheckedAt: Date.now()
|
|
2651
|
+
});
|
|
2652
|
+
}, [project_id]);
|
|
1034
2653
|
useEffect(() => {
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
if (refreshToken) {
|
|
1044
|
-
fetchToken(refreshToken, true).catch((error2) => {
|
|
1045
|
-
log("Retry refresh failed:", error2);
|
|
1046
|
-
});
|
|
1047
|
-
}
|
|
2654
|
+
const runVersionUpdater = async () => {
|
|
2655
|
+
try {
|
|
2656
|
+
const versionUpdater = createVersionUpdater(storageAdapter, version, getMigrations());
|
|
2657
|
+
const updateResult = await versionUpdater.checkAndUpdate();
|
|
2658
|
+
if (updateResult.updated) {
|
|
2659
|
+
log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`);
|
|
2660
|
+
} else {
|
|
2661
|
+
log(`App version ${updateResult.toVersion} is current`);
|
|
1048
2662
|
}
|
|
2663
|
+
} catch (error2) {
|
|
2664
|
+
log("Version update failed:", error2);
|
|
1049
2665
|
}
|
|
1050
2666
|
};
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
window.addEventListener("online", handleOnline);
|
|
1056
|
-
window.addEventListener("offline", handleOffline);
|
|
1057
|
-
return () => {
|
|
1058
|
-
window.removeEventListener("online", handleOnline);
|
|
1059
|
-
window.removeEventListener("offline", handleOffline);
|
|
1060
|
-
};
|
|
1061
|
-
}, [pendingRefresh, token]);
|
|
2667
|
+
runVersionUpdater();
|
|
2668
|
+
authRef.current.initialize();
|
|
2669
|
+
return authRef.current.setupNetworkListeners();
|
|
2670
|
+
}, []);
|
|
1062
2671
|
useEffect(() => {
|
|
1063
2672
|
async function initSyncDb(options) {
|
|
1064
2673
|
if (!syncRef.current) {
|
|
1065
2674
|
log("Initializing Basic Sync DB");
|
|
1066
2675
|
await initDexieExtensions();
|
|
1067
2676
|
syncRef.current = new BasicSync("basicdb", { schema });
|
|
1068
|
-
syncRef.current.syncable.on("statusChanged", (status
|
|
1069
|
-
|
|
2677
|
+
syncRef.current.syncable.on("statusChanged", (status) => {
|
|
2678
|
+
const newStatus = getSyncStatus(status);
|
|
2679
|
+
setDbStatus(newStatus);
|
|
2680
|
+
if (newStatus === "ERROR_WILL_RETRY" /* ERROR_WILL_RETRY */) {
|
|
2681
|
+
log("Sync entered ERROR_WILL_RETRY - proactively refreshing token");
|
|
2682
|
+
authRef.current.getToken({ forceRefresh: true }).catch(() => {
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
1070
2685
|
});
|
|
1071
2686
|
if (options.shouldConnect) {
|
|
1072
2687
|
setShouldConnect(true);
|
|
@@ -1089,14 +2704,14 @@ function BasicProvider({
|
|
|
1089
2704
|
}
|
|
1090
2705
|
log("Initializing Basic Remote DB");
|
|
1091
2706
|
remoteDbRef.current = new RemoteDB({
|
|
1092
|
-
serverUrl: authConfig.
|
|
2707
|
+
serverUrl: authConfig.pds_url,
|
|
1093
2708
|
projectId: project_id,
|
|
1094
|
-
getToken,
|
|
2709
|
+
getToken: (opts) => authRef.current.getToken(opts),
|
|
1095
2710
|
schema,
|
|
1096
2711
|
debug,
|
|
1097
2712
|
onAuthError: (error2) => {
|
|
1098
2713
|
log("RemoteDB auth error:", error2);
|
|
1099
|
-
|
|
2714
|
+
handleSignOut();
|
|
1100
2715
|
}
|
|
1101
2716
|
});
|
|
1102
2717
|
setDbStatus("ONLINE" /* ONLINE */);
|
|
@@ -1108,11 +2723,19 @@ function BasicProvider({
|
|
|
1108
2723
|
if (!result.isValid) {
|
|
1109
2724
|
let errorMessage = "";
|
|
1110
2725
|
if (result.errors) {
|
|
1111
|
-
result.errors.forEach((
|
|
1112
|
-
errorMessage += `${index + 1}: ${
|
|
2726
|
+
result.errors.forEach((err, index) => {
|
|
2727
|
+
errorMessage += `${index + 1}: ${err.message} - at ${err.instancePath}
|
|
1113
2728
|
`;
|
|
1114
2729
|
});
|
|
1115
2730
|
}
|
|
2731
|
+
setSchemaDevInfo({
|
|
2732
|
+
projectId: schema?.project_id ?? null,
|
|
2733
|
+
localVersion: schema?.version,
|
|
2734
|
+
status: "invalid",
|
|
2735
|
+
valid: false,
|
|
2736
|
+
lastCheckedAt: Date.now(),
|
|
2737
|
+
error: errorMessage.trim() || void 0
|
|
2738
|
+
});
|
|
1116
2739
|
setError({
|
|
1117
2740
|
code: "schema_invalid",
|
|
1118
2741
|
title: "Basic Schema is invalid!",
|
|
@@ -1121,13 +2744,24 @@ function BasicProvider({
|
|
|
1121
2744
|
setIsReady(true);
|
|
1122
2745
|
return null;
|
|
1123
2746
|
}
|
|
2747
|
+
setSchemaDevInfo({
|
|
2748
|
+
projectId: schema?.project_id ?? null,
|
|
2749
|
+
localVersion: schema?.version,
|
|
2750
|
+
status: result.schemaStatus.status ?? "unknown",
|
|
2751
|
+
valid: result.schemaStatus.valid,
|
|
2752
|
+
lastCheckedAt: Date.now()
|
|
2753
|
+
});
|
|
1124
2754
|
if (dbMode === "remote") {
|
|
1125
2755
|
initRemoteDb();
|
|
1126
2756
|
} else {
|
|
1127
2757
|
if (result.schemaStatus.valid) {
|
|
1128
2758
|
await initSyncDb({ shouldConnect: true });
|
|
1129
2759
|
} else {
|
|
1130
|
-
|
|
2760
|
+
if (result.schemaStatus.status === "unpublished") {
|
|
2761
|
+
log("Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync.");
|
|
2762
|
+
} else {
|
|
2763
|
+
log("Schema is invalid!", result.schemaStatus);
|
|
2764
|
+
}
|
|
1131
2765
|
await initSyncDb({ shouldConnect: false });
|
|
1132
2766
|
}
|
|
1133
2767
|
}
|
|
@@ -1136,6 +2770,15 @@ function BasicProvider({
|
|
|
1136
2770
|
if (schema) {
|
|
1137
2771
|
checkSchema();
|
|
1138
2772
|
} else {
|
|
2773
|
+
setSchemaDevInfo(
|
|
2774
|
+
project_id ? {
|
|
2775
|
+
projectId: project_id,
|
|
2776
|
+
localVersion: void 0,
|
|
2777
|
+
status: "no_schema",
|
|
2778
|
+
valid: false,
|
|
2779
|
+
lastCheckedAt: Date.now()
|
|
2780
|
+
} : null
|
|
2781
|
+
);
|
|
1139
2782
|
if (dbMode === "remote" && project_id) {
|
|
1140
2783
|
initRemoteDb();
|
|
1141
2784
|
} else {
|
|
@@ -1144,220 +2787,33 @@ function BasicProvider({
|
|
|
1144
2787
|
}
|
|
1145
2788
|
}, []);
|
|
1146
2789
|
useEffect(() => {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
syncRef.current?.connect({
|
|
1156
|
-
access_token: tok,
|
|
1157
|
-
ws_url: authConfig.ws_url
|
|
1158
|
-
}).catch((e) => {
|
|
1159
|
-
log("error connecting to db", e);
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
2790
|
+
if (authState.hasToken && syncRef.current && authState.isSignedIn && shouldConnect) {
|
|
2791
|
+
log("connecting to db...");
|
|
2792
|
+
syncRef.current?.connect({
|
|
2793
|
+
getToken: (opts) => authRef.current.getToken(opts),
|
|
2794
|
+
ws_url: authConfig.ws_url
|
|
2795
|
+
}).catch((e) => {
|
|
2796
|
+
log("error connecting to db", e);
|
|
2797
|
+
});
|
|
1162
2798
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
await storageAdapter.set(STORAGE_KEYS.DEBUG, debug ? "true" : "false");
|
|
1168
|
-
const storedServerUrl = await storageAdapter.get(STORAGE_KEYS.SERVER_URL);
|
|
1169
|
-
if (storedServerUrl && storedServerUrl !== authConfig.server_url) {
|
|
1170
|
-
log("Server URL changed, clearing stored tokens");
|
|
1171
|
-
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1172
|
-
await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
|
|
1173
|
-
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
|
|
1174
|
-
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
|
|
1175
|
-
clearCookie("basic_token");
|
|
1176
|
-
clearCookie("basic_access_token");
|
|
1177
|
-
}
|
|
1178
|
-
await storageAdapter.set(STORAGE_KEYS.SERVER_URL, authConfig.server_url);
|
|
1179
|
-
try {
|
|
1180
|
-
const versionUpdater = createVersionUpdater(storageAdapter, version, getMigrations());
|
|
1181
|
-
const updateResult = await versionUpdater.checkAndUpdate();
|
|
1182
|
-
if (updateResult.updated) {
|
|
1183
|
-
log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`);
|
|
1184
|
-
} else {
|
|
1185
|
-
log(`App version ${updateResult.toVersion} is current`);
|
|
1186
|
-
}
|
|
1187
|
-
} catch (error2) {
|
|
1188
|
-
log("Version update failed:", error2);
|
|
1189
|
-
}
|
|
1190
|
-
try {
|
|
1191
|
-
if (window.location.search.includes("code")) {
|
|
1192
|
-
let code = window.location?.search?.split("code=")[1]?.split("&")[0];
|
|
1193
|
-
if (!code)
|
|
1194
|
-
return;
|
|
1195
|
-
const state = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE);
|
|
1196
|
-
const urlState = window.location.search.split("state=")[1]?.split("&")[0];
|
|
1197
|
-
if (!state || state !== urlState) {
|
|
1198
|
-
log("error: auth state does not match");
|
|
1199
|
-
setIsAuthReady(true);
|
|
1200
|
-
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
|
|
1201
|
-
cleanOAuthParams();
|
|
1202
|
-
return;
|
|
1203
|
-
}
|
|
1204
|
-
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
|
|
1205
|
-
cleanOAuthParams();
|
|
1206
|
-
fetchToken(code, false).catch((error2) => {
|
|
1207
|
-
log("Error fetching token:", error2);
|
|
1208
|
-
});
|
|
1209
|
-
} else {
|
|
1210
|
-
const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1211
|
-
if (refreshToken) {
|
|
1212
|
-
log("Found refresh token in storage, attempting to refresh access token");
|
|
1213
|
-
fetchToken(refreshToken, true).catch((error2) => {
|
|
1214
|
-
log("Error fetching refresh token:", error2);
|
|
1215
|
-
});
|
|
1216
|
-
} else {
|
|
1217
|
-
let cookie_token = getCookie("basic_token");
|
|
1218
|
-
if (cookie_token !== "") {
|
|
1219
|
-
const tokenData = JSON.parse(cookie_token);
|
|
1220
|
-
setToken(tokenData);
|
|
1221
|
-
if (tokenData.refresh_token) {
|
|
1222
|
-
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, tokenData.refresh_token);
|
|
1223
|
-
}
|
|
1224
|
-
} else {
|
|
1225
|
-
const cachedUserInfo = await storageAdapter.get(STORAGE_KEYS.USER_INFO);
|
|
1226
|
-
if (cachedUserInfo) {
|
|
1227
|
-
try {
|
|
1228
|
-
const userData = JSON.parse(cachedUserInfo);
|
|
1229
|
-
setUser(userData);
|
|
1230
|
-
setIsSignedIn(true);
|
|
1231
|
-
log("Loaded cached user info for offline mode");
|
|
1232
|
-
} catch (error2) {
|
|
1233
|
-
log("Error parsing cached user info:", error2);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
setIsAuthReady(true);
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
} catch (e) {
|
|
1241
|
-
log("error getting token", e);
|
|
1242
|
-
}
|
|
1243
|
-
};
|
|
1244
|
-
initializeAuth();
|
|
1245
|
-
}, []);
|
|
1246
|
-
useEffect(() => {
|
|
1247
|
-
async function fetchUser(acc_token) {
|
|
1248
|
-
console.info("fetching user");
|
|
2799
|
+
}, [authState.isSignedIn, authState.hasToken, shouldConnect]);
|
|
2800
|
+
const handleSignOut = async () => {
|
|
2801
|
+
await authRef.current.signOut();
|
|
2802
|
+
if (syncRef.current) {
|
|
1249
2803
|
try {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
if (!response.ok) {
|
|
1257
|
-
throw new Error(`Failed to fetch user info: ${response.status}`);
|
|
1258
|
-
}
|
|
1259
|
-
const user2 = await response.json();
|
|
1260
|
-
if (user2.error) {
|
|
1261
|
-
log("error fetching user", user2.error);
|
|
1262
|
-
throw new Error(`User info error: ${user2.error}`);
|
|
1263
|
-
}
|
|
1264
|
-
if (token?.refresh_token) {
|
|
1265
|
-
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token);
|
|
1266
|
-
}
|
|
1267
|
-
await storageAdapter.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user2));
|
|
1268
|
-
log("Cached user info in storage");
|
|
1269
|
-
setCookie("basic_access_token", token?.access_token || "", { httpOnly: false });
|
|
1270
|
-
setCookie("basic_token", JSON.stringify(token));
|
|
1271
|
-
setUser(user2);
|
|
1272
|
-
setIsSignedIn(true);
|
|
1273
|
-
setIsAuthReady(true);
|
|
2804
|
+
await syncRef.current.close();
|
|
2805
|
+
await syncRef.current.delete({ disableAutoOpen: false });
|
|
2806
|
+
syncRef.current = null;
|
|
2807
|
+
window?.location?.reload();
|
|
1274
2808
|
} catch (error2) {
|
|
1275
|
-
|
|
1276
|
-
setIsAuthReady(true);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
async function checkToken() {
|
|
1280
|
-
if (!token) {
|
|
1281
|
-
log("error: no user token found");
|
|
1282
|
-
setIsAuthReady(true);
|
|
1283
|
-
return;
|
|
2809
|
+
console.error("Error during database cleanup:", error2);
|
|
1284
2810
|
}
|
|
1285
|
-
const decoded = jwtDecode(token?.access_token);
|
|
1286
|
-
const expirationBuffer = 5;
|
|
1287
|
-
const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
|
|
1288
|
-
if (isExpired) {
|
|
1289
|
-
log("token is expired - refreshing ...");
|
|
1290
|
-
const refreshToken = token?.refresh_token;
|
|
1291
|
-
if (!refreshToken) {
|
|
1292
|
-
log("Error: No refresh token available for expired token");
|
|
1293
|
-
setIsAuthReady(true);
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
try {
|
|
1297
|
-
const newToken = await fetchToken(refreshToken, true);
|
|
1298
|
-
fetchUser(newToken?.access_token || "");
|
|
1299
|
-
} catch (error2) {
|
|
1300
|
-
log("Failed to refresh token in checkToken:", error2);
|
|
1301
|
-
if (error2.message.includes("offline") || error2.message.includes("Network")) {
|
|
1302
|
-
log("Network issue - continuing with expired token until online");
|
|
1303
|
-
fetchUser(token?.access_token || "");
|
|
1304
|
-
} else {
|
|
1305
|
-
setIsAuthReady(true);
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
} else {
|
|
1309
|
-
fetchUser(token?.access_token || "");
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
if (token) {
|
|
1313
|
-
checkToken();
|
|
1314
|
-
}
|
|
1315
|
-
}, [token]);
|
|
1316
|
-
const getSignInLink = async (redirectUri) => {
|
|
1317
|
-
try {
|
|
1318
|
-
log("getting sign in link...");
|
|
1319
|
-
if (!project_id) {
|
|
1320
|
-
throw new Error("Project ID is required to generate sign-in link");
|
|
1321
|
-
}
|
|
1322
|
-
const randomState = Math.random().toString(36).substring(6);
|
|
1323
|
-
await storageAdapter.set(STORAGE_KEYS.AUTH_STATE, randomState);
|
|
1324
|
-
const redirectUrl = redirectUri || window.location.href;
|
|
1325
|
-
if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
|
|
1326
|
-
throw new Error("Invalid redirect URI provided");
|
|
1327
|
-
}
|
|
1328
|
-
await storageAdapter.set(STORAGE_KEYS.REDIRECT_URI, redirectUrl);
|
|
1329
|
-
log("Stored redirect_uri for token exchange:", redirectUrl);
|
|
1330
|
-
let baseUrl = `${authConfig.server_url}/auth/authorize`;
|
|
1331
|
-
baseUrl += `?client_id=${project_id}`;
|
|
1332
|
-
baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`;
|
|
1333
|
-
baseUrl += `&response_type=code`;
|
|
1334
|
-
baseUrl += `&scope=${encodeURIComponent(scopesString)}`;
|
|
1335
|
-
baseUrl += `&state=${randomState}`;
|
|
1336
|
-
log("Generated sign-in link successfully with scopes:", scopesString);
|
|
1337
|
-
return baseUrl;
|
|
1338
|
-
} catch (error2) {
|
|
1339
|
-
log("Error generating sign-in link:", error2);
|
|
1340
|
-
throw error2;
|
|
1341
2811
|
}
|
|
1342
2812
|
};
|
|
1343
|
-
const
|
|
2813
|
+
const handleSignIn = async () => {
|
|
1344
2814
|
try {
|
|
1345
|
-
|
|
1346
|
-
if (!project_id) {
|
|
1347
|
-
log("Error: project_id is required for sign-in");
|
|
1348
|
-
throw new Error("Project ID is required for authentication");
|
|
1349
|
-
}
|
|
1350
|
-
const signInLink = await getSignInLink();
|
|
1351
|
-
log("Generated sign-in link:", signInLink);
|
|
1352
|
-
try {
|
|
1353
|
-
new URL(signInLink);
|
|
1354
|
-
} catch {
|
|
1355
|
-
log("Error: Invalid sign-in link generated");
|
|
1356
|
-
throw new Error("Failed to generate valid sign-in URL");
|
|
1357
|
-
}
|
|
1358
|
-
window.location.href = signInLink;
|
|
2815
|
+
await authRef.current.signIn();
|
|
1359
2816
|
} catch (error2) {
|
|
1360
|
-
log("Error during sign-in:", error2);
|
|
1361
2817
|
if (isDevMode()) {
|
|
1362
2818
|
setError({
|
|
1363
2819
|
code: "signin_error",
|
|
@@ -1368,255 +2824,19 @@ function BasicProvider({
|
|
|
1368
2824
|
throw error2;
|
|
1369
2825
|
}
|
|
1370
2826
|
};
|
|
1371
|
-
const
|
|
2827
|
+
const handleSignInWithHandle = async (handle) => {
|
|
1372
2828
|
try {
|
|
1373
|
-
|
|
1374
|
-
if (!code || typeof code !== "string") {
|
|
1375
|
-
return { success: false, error: "Invalid authorization code" };
|
|
1376
|
-
}
|
|
1377
|
-
if (state) {
|
|
1378
|
-
const storedState = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE);
|
|
1379
|
-
if (storedState && storedState !== state) {
|
|
1380
|
-
log("State parameter mismatch:", { provided: state, stored: storedState });
|
|
1381
|
-
return { success: false, error: "State parameter mismatch" };
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
|
|
1385
|
-
cleanOAuthParams();
|
|
1386
|
-
const token2 = await fetchToken(code, false);
|
|
1387
|
-
if (token2) {
|
|
1388
|
-
log("signinWithCode successful");
|
|
1389
|
-
return { success: true };
|
|
1390
|
-
} else {
|
|
1391
|
-
return { success: false, error: "Failed to exchange code for token" };
|
|
1392
|
-
}
|
|
2829
|
+
await authRef.current.signInWithHandle(handle);
|
|
1393
2830
|
} catch (error2) {
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
}
|
|
1400
|
-
};
|
|
1401
|
-
const signout = async () => {
|
|
1402
|
-
log("signing out!");
|
|
1403
|
-
setUser({});
|
|
1404
|
-
setIsSignedIn(false);
|
|
1405
|
-
setToken(null);
|
|
1406
|
-
clearCookie("basic_token");
|
|
1407
|
-
clearCookie("basic_access_token");
|
|
1408
|
-
await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
|
|
1409
|
-
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1410
|
-
await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
|
|
1411
|
-
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
|
|
1412
|
-
await storageAdapter.remove(STORAGE_KEYS.SERVER_URL);
|
|
1413
|
-
if (syncRef.current) {
|
|
1414
|
-
(async () => {
|
|
1415
|
-
try {
|
|
1416
|
-
await syncRef.current?.close();
|
|
1417
|
-
await syncRef.current?.delete({ disableAutoOpen: false });
|
|
1418
|
-
syncRef.current = null;
|
|
1419
|
-
window?.location?.reload();
|
|
1420
|
-
} catch (error2) {
|
|
1421
|
-
console.error("Error during database cleanup:", error2);
|
|
1422
|
-
}
|
|
1423
|
-
})();
|
|
1424
|
-
}
|
|
1425
|
-
};
|
|
1426
|
-
const getToken = async () => {
|
|
1427
|
-
log("getting token...");
|
|
1428
|
-
if (!token) {
|
|
1429
|
-
const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1430
|
-
if (refreshToken) {
|
|
1431
|
-
log("No token in memory, attempting to refresh from storage");
|
|
1432
|
-
if (refreshPromiseRef.current) {
|
|
1433
|
-
log("Token refresh already in progress, waiting...");
|
|
1434
|
-
try {
|
|
1435
|
-
const newToken = await refreshPromiseRef.current;
|
|
1436
|
-
if (newToken?.access_token) {
|
|
1437
|
-
return newToken.access_token;
|
|
1438
|
-
}
|
|
1439
|
-
} catch (error2) {
|
|
1440
|
-
log("In-flight refresh failed:", error2);
|
|
1441
|
-
throw error2;
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
try {
|
|
1445
|
-
const newToken = await fetchToken(refreshToken, true);
|
|
1446
|
-
if (newToken?.access_token) {
|
|
1447
|
-
return newToken.access_token;
|
|
1448
|
-
}
|
|
1449
|
-
} catch (error2) {
|
|
1450
|
-
log("Failed to refresh token from storage:", error2);
|
|
1451
|
-
if (error2.message.includes("offline") || error2.message.includes("Network")) {
|
|
1452
|
-
log("Network issue - continuing with potentially expired token");
|
|
1453
|
-
const lastToken = localStorage.getItem("basic_access_token");
|
|
1454
|
-
if (lastToken) {
|
|
1455
|
-
return lastToken;
|
|
1456
|
-
}
|
|
1457
|
-
throw new Error("Network offline - authentication will be retried when online");
|
|
1458
|
-
}
|
|
1459
|
-
throw new Error("Authentication expired. Please sign in again.");
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
log("no token found");
|
|
1463
|
-
throw new Error("no token found");
|
|
1464
|
-
}
|
|
1465
|
-
const decoded = jwtDecode(token?.access_token);
|
|
1466
|
-
const expirationBuffer = 5;
|
|
1467
|
-
const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
|
|
1468
|
-
if (isExpired) {
|
|
1469
|
-
log("token is expired - refreshing ...");
|
|
1470
|
-
if (refreshPromiseRef.current) {
|
|
1471
|
-
log("Token refresh already in progress, waiting...");
|
|
1472
|
-
try {
|
|
1473
|
-
const newToken = await refreshPromiseRef.current;
|
|
1474
|
-
return newToken?.access_token || "";
|
|
1475
|
-
} catch (error2) {
|
|
1476
|
-
log("In-flight refresh failed:", error2);
|
|
1477
|
-
if (error2.message.includes("offline") || error2.message.includes("Network")) {
|
|
1478
|
-
log("Network issue - using expired token until network is restored");
|
|
1479
|
-
return token.access_token;
|
|
1480
|
-
}
|
|
1481
|
-
throw error2;
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
const refreshToken = token?.refresh_token || await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1485
|
-
if (refreshToken) {
|
|
1486
|
-
try {
|
|
1487
|
-
const newToken = await fetchToken(refreshToken, true);
|
|
1488
|
-
return newToken?.access_token || "";
|
|
1489
|
-
} catch (error2) {
|
|
1490
|
-
log("Failed to refresh expired token:", error2);
|
|
1491
|
-
if (error2.message.includes("offline") || error2.message.includes("Network")) {
|
|
1492
|
-
log("Network issue - using expired token until network is restored");
|
|
1493
|
-
return token.access_token;
|
|
1494
|
-
}
|
|
1495
|
-
throw new Error("Authentication expired. Please sign in again.");
|
|
1496
|
-
}
|
|
1497
|
-
} else {
|
|
1498
|
-
throw new Error("no refresh token available");
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
return token?.access_token || "";
|
|
1502
|
-
};
|
|
1503
|
-
const fetchToken = async (codeOrRefreshToken, isRefreshToken = false) => {
|
|
1504
|
-
if (!codeOrRefreshToken || codeOrRefreshToken.trim() === "") {
|
|
1505
|
-
const errorMsg = isRefreshToken ? "Refresh token is empty or undefined" : "Authorization code is empty or undefined";
|
|
1506
|
-
log("Error:", errorMsg);
|
|
1507
|
-
throw new Error(errorMsg);
|
|
1508
|
-
}
|
|
1509
|
-
if (isRefreshToken && refreshPromiseRef.current) {
|
|
1510
|
-
log("Reusing in-flight refresh token request");
|
|
1511
|
-
return refreshPromiseRef.current;
|
|
1512
|
-
}
|
|
1513
|
-
const refreshPromise = (async () => {
|
|
1514
|
-
try {
|
|
1515
|
-
if (!isOnline) {
|
|
1516
|
-
log("Network is offline, marking refresh as pending");
|
|
1517
|
-
setPendingRefresh(true);
|
|
1518
|
-
throw new Error("Network offline - refresh will be retried when online");
|
|
1519
|
-
}
|
|
1520
|
-
let requestBody;
|
|
1521
|
-
if (isRefreshToken) {
|
|
1522
|
-
requestBody = {
|
|
1523
|
-
grant_type: "refresh_token",
|
|
1524
|
-
refresh_token: codeOrRefreshToken
|
|
1525
|
-
};
|
|
1526
|
-
if (project_id) {
|
|
1527
|
-
requestBody.client_id = project_id;
|
|
1528
|
-
}
|
|
1529
|
-
} else {
|
|
1530
|
-
requestBody = {
|
|
1531
|
-
grant_type: "authorization_code",
|
|
1532
|
-
code: codeOrRefreshToken
|
|
1533
|
-
};
|
|
1534
|
-
const storedRedirectUri = await storageAdapter.get(STORAGE_KEYS.REDIRECT_URI);
|
|
1535
|
-
if (storedRedirectUri) {
|
|
1536
|
-
requestBody.redirect_uri = storedRedirectUri;
|
|
1537
|
-
log("Including redirect_uri in token exchange:", storedRedirectUri);
|
|
1538
|
-
} else {
|
|
1539
|
-
log("Warning: No redirect_uri found in storage for token exchange");
|
|
1540
|
-
}
|
|
1541
|
-
if (project_id) {
|
|
1542
|
-
requestBody.client_id = project_id;
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
log("Token exchange request body:", { ...requestBody, refresh_token: isRefreshToken ? "[REDACTED]" : void 0, code: !isRefreshToken ? "[REDACTED]" : void 0 });
|
|
1546
|
-
const token2 = await fetch(`${authConfig.server_url}/auth/token`, {
|
|
1547
|
-
method: "POST",
|
|
1548
|
-
headers: {
|
|
1549
|
-
"Content-Type": "application/json"
|
|
1550
|
-
},
|
|
1551
|
-
body: JSON.stringify(requestBody)
|
|
1552
|
-
}).then((response) => response.json()).catch((error2) => {
|
|
1553
|
-
log("Network error fetching token:", error2);
|
|
1554
|
-
if (!isOnline) {
|
|
1555
|
-
setPendingRefresh(true);
|
|
1556
|
-
throw new Error("Network offline - refresh will be retried when online");
|
|
1557
|
-
}
|
|
1558
|
-
throw new Error("Network error during token refresh");
|
|
2831
|
+
if (isDevMode()) {
|
|
2832
|
+
setError({
|
|
2833
|
+
code: "signin_error",
|
|
2834
|
+
title: "Sign-in Failed",
|
|
2835
|
+
message: error2.message || "An error occurred during sign-in. Please try again."
|
|
1559
2836
|
});
|
|
1560
|
-
if (token2.error) {
|
|
1561
|
-
log("error fetching token", token2.error);
|
|
1562
|
-
if (token2.error.includes("network") || token2.error.includes("timeout")) {
|
|
1563
|
-
setPendingRefresh(true);
|
|
1564
|
-
throw new Error("Network issue - refresh will be retried when online");
|
|
1565
|
-
}
|
|
1566
|
-
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1567
|
-
await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
|
|
1568
|
-
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
|
|
1569
|
-
await storageAdapter.remove(STORAGE_KEYS.SERVER_URL);
|
|
1570
|
-
clearCookie("basic_token");
|
|
1571
|
-
clearCookie("basic_access_token");
|
|
1572
|
-
setUser({});
|
|
1573
|
-
setIsSignedIn(false);
|
|
1574
|
-
setToken(null);
|
|
1575
|
-
setIsAuthReady(true);
|
|
1576
|
-
throw new Error(`Token refresh failed: ${token2.error}`);
|
|
1577
|
-
} else {
|
|
1578
|
-
setToken(token2);
|
|
1579
|
-
setPendingRefresh(false);
|
|
1580
|
-
if (token2.refresh_token) {
|
|
1581
|
-
await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token2.refresh_token);
|
|
1582
|
-
log("Updated refresh token in storage");
|
|
1583
|
-
}
|
|
1584
|
-
if (!isRefreshToken) {
|
|
1585
|
-
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
|
|
1586
|
-
log("Cleaned up redirect_uri from storage after successful exchange");
|
|
1587
|
-
}
|
|
1588
|
-
setCookie("basic_access_token", token2.access_token, { httpOnly: false });
|
|
1589
|
-
setCookie("basic_token", JSON.stringify(token2));
|
|
1590
|
-
log("Updated access token and full token in cookies");
|
|
1591
|
-
}
|
|
1592
|
-
return token2;
|
|
1593
|
-
} catch (error2) {
|
|
1594
|
-
log("Token refresh error:", error2);
|
|
1595
|
-
if (!error2.message.includes("offline") && !error2.message.includes("Network")) {
|
|
1596
|
-
await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
|
|
1597
|
-
await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
|
|
1598
|
-
await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
|
|
1599
|
-
await storageAdapter.remove(STORAGE_KEYS.SERVER_URL);
|
|
1600
|
-
clearCookie("basic_token");
|
|
1601
|
-
clearCookie("basic_access_token");
|
|
1602
|
-
setUser({});
|
|
1603
|
-
setIsSignedIn(false);
|
|
1604
|
-
setToken(null);
|
|
1605
|
-
setIsAuthReady(true);
|
|
1606
|
-
}
|
|
1607
|
-
throw error2;
|
|
1608
2837
|
}
|
|
1609
|
-
|
|
1610
|
-
if (isRefreshToken) {
|
|
1611
|
-
refreshPromiseRef.current = refreshPromise;
|
|
1612
|
-
refreshPromise.finally(() => {
|
|
1613
|
-
if (refreshPromiseRef.current === refreshPromise) {
|
|
1614
|
-
refreshPromiseRef.current = null;
|
|
1615
|
-
log("Cleared refresh promise reference");
|
|
1616
|
-
}
|
|
1617
|
-
});
|
|
2838
|
+
throw error2;
|
|
1618
2839
|
}
|
|
1619
|
-
return refreshPromise;
|
|
1620
2840
|
};
|
|
1621
2841
|
const getCurrentDb = () => {
|
|
1622
2842
|
if (dbMode === "remote") {
|
|
@@ -1625,69 +2845,81 @@ function BasicProvider({
|
|
|
1625
2845
|
return syncRef.current || noDb;
|
|
1626
2846
|
};
|
|
1627
2847
|
const contextValue = {
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
2848
|
+
isReady: authState.isAuthReady,
|
|
2849
|
+
isSignedIn: authState.isSignedIn,
|
|
2850
|
+
user: authState.user,
|
|
2851
|
+
did: authState.did,
|
|
2852
|
+
scope: authState.tokenScope,
|
|
2853
|
+
hasScope: (s) => authRef.current.hasScope(s),
|
|
2854
|
+
missingScopes: () => authRef.current.missingScopes(),
|
|
2855
|
+
signIn: handleSignIn,
|
|
2856
|
+
signInWithHandle: handleSignInWithHandle,
|
|
2857
|
+
signOut: handleSignOut,
|
|
2858
|
+
signInWithCode: (code, state) => authRef.current.signInWithCode(code, state),
|
|
2859
|
+
getToken: (opts) => authRef.current.getToken(opts),
|
|
2860
|
+
getSignInUrl: (redirectUri) => authRef.current.getSignInUrl(redirectUri),
|
|
1640
2861
|
db: getCurrentDb(),
|
|
1641
2862
|
dbStatus,
|
|
1642
2863
|
dbMode,
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
2864
|
+
devInfo: schemaDevInfo,
|
|
2865
|
+
refreshSchemaStatus,
|
|
2866
|
+
isAuthReady: authState.isAuthReady,
|
|
2867
|
+
signin: handleSignIn,
|
|
2868
|
+
signout: handleSignOut,
|
|
2869
|
+
signinWithCode: (code, state) => authRef.current.signInWithCode(code, state),
|
|
2870
|
+
getSignInLink: (redirectUri) => authRef.current.getSignInUrl(redirectUri)
|
|
1649
2871
|
};
|
|
1650
|
-
return /* @__PURE__ */
|
|
1651
|
-
error && isDevMode() && /* @__PURE__ */
|
|
2872
|
+
return /* @__PURE__ */ jsxs2(BasicContext.Provider, { value: contextValue, children: [
|
|
2873
|
+
error && isDevMode() && /* @__PURE__ */ jsx2(ErrorDisplay, { error }),
|
|
2874
|
+
devToolbar && isDevMode() && /* @__PURE__ */ jsx2(Suspense, { fallback: null, children: /* @__PURE__ */ jsx2(BasicDevToolbar2, { debug }) }),
|
|
1652
2875
|
isReady && children
|
|
1653
2876
|
] });
|
|
1654
2877
|
}
|
|
1655
2878
|
function ErrorDisplay({ error }) {
|
|
1656
|
-
return /* @__PURE__ */
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
|
|
2879
|
+
return /* @__PURE__ */ jsxs2(
|
|
2880
|
+
"div",
|
|
2881
|
+
{
|
|
2882
|
+
style: {
|
|
2883
|
+
position: "absolute",
|
|
2884
|
+
top: 20,
|
|
2885
|
+
left: 20,
|
|
2886
|
+
color: "black",
|
|
2887
|
+
backgroundColor: "#f8d7da",
|
|
2888
|
+
border: "1px solid #f5c6cb",
|
|
2889
|
+
borderRadius: "4px",
|
|
2890
|
+
padding: "20px",
|
|
2891
|
+
maxWidth: "400px",
|
|
2892
|
+
margin: "20px auto",
|
|
2893
|
+
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
|
2894
|
+
fontFamily: "monospace"
|
|
2895
|
+
},
|
|
2896
|
+
children: [
|
|
2897
|
+
/* @__PURE__ */ jsxs2("h3", { style: { fontSize: "0.8rem", opacity: 0.8 }, children: [
|
|
2898
|
+
"code: ",
|
|
2899
|
+
error.code
|
|
2900
|
+
] }),
|
|
2901
|
+
/* @__PURE__ */ jsx2("h1", { style: { fontSize: "1.2rem", lineHeight: 1.5 }, children: error.title }),
|
|
2902
|
+
/* @__PURE__ */ jsx2("p", { children: error.message })
|
|
2903
|
+
]
|
|
2904
|
+
}
|
|
2905
|
+
);
|
|
1680
2906
|
}
|
|
1681
2907
|
|
|
1682
2908
|
// src/index.ts
|
|
2909
|
+
init_BasicDevToolbar();
|
|
1683
2910
|
import { useLiveQuery as useQuery } from "dexie-react-hooks";
|
|
1684
2911
|
export {
|
|
2912
|
+
BasicDevToolbar,
|
|
1685
2913
|
BasicProvider,
|
|
2914
|
+
DBStatus,
|
|
1686
2915
|
NotAuthenticatedError,
|
|
1687
2916
|
RemoteCollection,
|
|
1688
2917
|
RemoteDB,
|
|
1689
2918
|
RemoteDBError,
|
|
1690
2919
|
STORAGE_KEYS,
|
|
2920
|
+
resolveDid,
|
|
2921
|
+
resolveDidWebUrl,
|
|
2922
|
+
resolveHandle,
|
|
1691
2923
|
useBasic,
|
|
1692
2924
|
useQuery
|
|
1693
2925
|
};
|