@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/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
- ws.onopen = function(event) {
63
- log("Opening socket - sending clientIdentity", context.clientIdentity);
64
- ws.send(
65
- JSON.stringify({
66
- type: "clientIdentity",
67
- clientIdentity: context.clientIdentity || null,
68
- authToken: options.authToken,
69
- schema: options.schema
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, { acceptCallback, isFirstRound });
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
- onError(requestFromServer.message, Infinity);
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 { createContext, useContext, useEffect, useState, useRef } from "react";
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
- return;
160
- if (typeof window === "undefined")
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({ access_token, ws_url }) {
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, { authToken: access_token, schema: this.basic_schema });
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(([key2, field]) => field.indexed).map(([key2, field]) => `,${key2}`).join("");
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, retrying with fresh token...");
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 (response.status === 401 && this.config.onAuthError) {
418
- this.config.onAuthError({
419
- status: response.status,
420
- message: "Authentication failed",
421
- response: responseData
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/AuthContext.tsx
622
- init_config();
1509
+ // src/core/auth/AuthManager.ts
1510
+ import { jwtDecode } from "jwt-decode";
623
1511
 
624
- // package.json
625
- var version = "0.7.0-beta.6";
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/updater/versionUpdater.ts
628
- var VersionUpdater = class {
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
- currentVersion;
631
- migrations;
632
- versionKey = "basic_app_version";
633
- constructor(storage, currentVersion, migrations = []) {
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.currentVersion = currentVersion;
636
- this.migrations = migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
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
- console.log(`Running migration from ${migration.fromVersion} to ${migration.toVersion}`);
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
- console.log(` Should run: ${shouldRun}`);
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
- console.log("Running test migration");
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/utils/storage.ts
753
- var LocalStorageAdapter = class {
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 as validateSchema2, compareSchemas } from "@basictech/schema";
2444
+ import { validateSchema, compareSchemas } from "@basictech/schema";
872
2445
  async function getSchemaStatus(schema) {
873
2446
  const projectId = schema.project_id;
874
- const valid = validateSchema2(schema);
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 = validateSchema2(schema);
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
- import { jsx, jsxs } from "react/jsx-runtime";
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
- server_url: "https://api.basic.tech",
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
- var noDb = {
975
- collection: () => {
976
- throw new Error("no basicdb found - initialization failed. double check your schema.");
977
- }
978
- };
979
- var BasicContext = createContext({
980
- // Auth state
981
- isReady: false,
982
- isSignedIn: false,
983
- user: null,
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
- const [isAuthReady, setIsAuthReady] = useState(false);
1013
- const [isSignedIn, setIsSignedIn] = useState(false);
1014
- const [token, setToken] = useState(null);
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
- server_url: auth?.server_url || DEFAULT_AUTH_CONFIG.server_url,
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 refreshPromiseRef = useRef(null);
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 cleanOAuthParams = () => cleanOAuthParamsFromUrl();
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 handleOnline = () => {
1036
- log("Network came back online");
1037
- setIsOnline(true);
1038
- if (pendingRefresh) {
1039
- log("Retrying pending token refresh");
1040
- setPendingRefresh(false);
1041
- if (token) {
1042
- const refreshToken = token.refresh_token || localStorage.getItem("basic_refresh_token");
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
- const handleOffline = () => {
1052
- log("Network went offline");
1053
- setIsOnline(false);
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, url) => {
1069
- setDbStatus(getSyncStatus(status));
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.server_url,
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
- signout();
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((error2, index) => {
1112
- errorMessage += `${index + 1}: ${error2.message} - at ${error2.instancePath}
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
- log("Schema is invalid!", result.schemaStatus);
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
- async function connectToDb() {
1148
- if (token && syncRef.current && isSignedIn && shouldConnect) {
1149
- const tok = await getToken();
1150
- if (!tok) {
1151
- log("no token found");
1152
- return;
1153
- }
1154
- log("connecting to db...");
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
- connectToDb();
1164
- }, [isSignedIn, shouldConnect]);
1165
- useEffect(() => {
1166
- const initializeAuth = async () => {
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
- const response = await fetch(`${authConfig.server_url}/auth/userInfo`, {
1251
- method: "GET",
1252
- headers: {
1253
- "Authorization": `Bearer ${acc_token}`
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
- log("Failed to fetch user info:", error2);
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 signin = async () => {
2813
+ const handleSignIn = async () => {
1344
2814
  try {
1345
- log("signing in...");
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 signinWithCode = async (code, state) => {
2827
+ const handleSignInWithHandle = async (handle) => {
1372
2828
  try {
1373
- log("signinWithCode called with code:", code);
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
- log("signinWithCode error:", error2);
1395
- return {
1396
- success: false,
1397
- error: error2.message || "Authentication failed"
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
- // Auth state (new naming)
1629
- isReady: isAuthReady,
1630
- isSignedIn,
1631
- user,
1632
- // Auth actions (new camelCase naming)
1633
- signIn: signin,
1634
- signOut: signout,
1635
- signInWithCode: signinWithCode,
1636
- // Token management
1637
- getToken,
1638
- getSignInUrl: getSignInLink,
1639
- // DB access
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
- // Legacy aliases (deprecated)
1644
- isAuthReady,
1645
- signin,
1646
- signout,
1647
- signinWithCode,
1648
- getSignInLink
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__ */ jsxs(BasicContext.Provider, { value: contextValue, children: [
1651
- error && isDevMode() && /* @__PURE__ */ jsx(ErrorDisplay, { error }),
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__ */ jsxs("div", { style: {
1657
- position: "absolute",
1658
- top: 20,
1659
- left: 20,
1660
- color: "black",
1661
- backgroundColor: "#f8d7da",
1662
- border: "1px solid #f5c6cb",
1663
- borderRadius: "4px",
1664
- padding: "20px",
1665
- maxWidth: "400px",
1666
- margin: "20px auto",
1667
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
1668
- fontFamily: "monospace"
1669
- }, children: [
1670
- /* @__PURE__ */ jsxs("h3", { style: { fontSize: "0.8rem", opacity: 0.8 }, children: [
1671
- "code: ",
1672
- error.code
1673
- ] }),
1674
- /* @__PURE__ */ jsx("h1", { style: { fontSize: "1.2rem", lineHeight: "1.5" }, children: error.title }),
1675
- /* @__PURE__ */ jsx("p", { children: error.message })
1676
- ] });
1677
- }
1678
- function useBasic() {
1679
- return useContext(BasicContext);
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
  };