@basictech/react 0.8.0-beta.1 → 0.8.0-beta.3

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
@@ -137,14 +137,37 @@ var init_syncProtocol = __esm({
137
137
  onError("Authentication failed: " + (err.message || err), RECONNECT_DELAY);
138
138
  }
139
139
  };
140
+ function handleVisibilityResume() {
141
+ if (document.visibilityState === "visible" && ws.readyState === WebSocket.OPEN) {
142
+ log("Page became visible - refreshing token for WebSocket");
143
+ resolveGetToken()({ forceRefresh: true }).then(function(newToken) {
144
+ if (ws.readyState === WebSocket.OPEN) {
145
+ ws.send(JSON.stringify({ type: "tokenUpdate", authToken: newToken }));
146
+ scheduleTokenRefresh(newToken);
147
+ }
148
+ }).catch(function(err) {
149
+ log("Token refresh on visibility resume failed:", err);
150
+ });
151
+ }
152
+ }
153
+ if (typeof document !== "undefined") {
154
+ document.addEventListener("visibilitychange", handleVisibilityResume);
155
+ }
156
+ function cleanupVisibilityListener() {
157
+ if (typeof document !== "undefined") {
158
+ document.removeEventListener("visibilitychange", handleVisibilityResume);
159
+ }
160
+ }
140
161
  ws.onerror = function(event) {
141
162
  clearRefreshTimer();
163
+ cleanupVisibilityListener();
142
164
  ws.close();
143
165
  log("ws.onerror", event);
144
166
  onError(event?.message, RECONNECT_DELAY);
145
167
  };
146
168
  ws.onclose = function(event) {
147
169
  clearRefreshTimer();
170
+ cleanupVisibilityListener();
148
171
  onError("Socket closed: " + event.reason, RECONNECT_DELAY);
149
172
  };
150
173
  var isFirstRound = true;
@@ -181,6 +204,7 @@ var init_syncProtocol = __esm({
181
204
  },
182
205
  disconnect: function() {
183
206
  clearRefreshTimer();
207
+ cleanupVisibilityListener();
184
208
  ws.close();
185
209
  }
186
210
  });
@@ -216,8 +240,809 @@ var init_syncProtocol = __esm({
216
240
  }
217
241
  });
218
242
 
243
+ // package.json
244
+ var version;
245
+ var init_package = __esm({
246
+ "package.json"() {
247
+ version = "0.8.0-beta.3";
248
+ }
249
+ });
250
+
251
+ // src/utils/network.ts
252
+ import semver from "semver";
253
+ function isDevelopment(debug) {
254
+ if (debug === true) return true;
255
+ if (typeof process !== "undefined" && process.env.NODE_ENV === "development") return true;
256
+ if (typeof window === "undefined" || !window.location) return false;
257
+ const host = window.location.hostname;
258
+ return host === "localhost" || host === "127.0.0.1" || host.includes("localhost") || host.includes("127.0.0.1") || host.includes(".local");
259
+ }
260
+ function normalizeVersion(v) {
261
+ if (v == null) return null;
262
+ const t = String(v).trim();
263
+ return t.length ? t : null;
264
+ }
265
+ function versionsMatch(a, b) {
266
+ const na = a.trim();
267
+ const nb = b.trim();
268
+ if (na === nb) return true;
269
+ const va = semver.valid(na);
270
+ const vb = semver.valid(nb);
271
+ if (va && vb) return semver.eq(va, vb);
272
+ return false;
273
+ }
274
+ function usesBetaDistTag(version2) {
275
+ const pre = semver.prerelease(version2);
276
+ const id = pre?.[0];
277
+ return typeof id === "string" && id.toLowerCase() === "beta";
278
+ }
279
+ async function checkForNewVersion() {
280
+ try {
281
+ const currentVersion = normalizeVersion(version);
282
+ if (!currentVersion) {
283
+ return { hasNewVersion: false, latestVersion: null, currentVersion: null };
284
+ }
285
+ const response = await fetch("https://registry.npmjs.org/@basictech/react", {
286
+ headers: { Accept: "application/vnd.npm.install-v1+json" }
287
+ });
288
+ if (!response.ok) {
289
+ throw new Error("Failed to fetch version from npm");
290
+ }
291
+ const data = await response.json();
292
+ const distTags = data["dist-tags"] ?? {};
293
+ const rawRegistry = usesBetaDistTag(currentVersion) ? distTags.beta ?? distTags.latest : distTags.latest;
294
+ const latestVersion = normalizeVersion(rawRegistry ?? null);
295
+ if (!latestVersion) {
296
+ throw new Error("Missing dist-tags from npm registry");
297
+ }
298
+ const same = versionsMatch(currentVersion, latestVersion);
299
+ if (!same && isDevelopment()) {
300
+ log("[basic] version check mismatch:", {
301
+ currentVersion,
302
+ registryVersion: latestVersion,
303
+ channel: usesBetaDistTag(currentVersion) ? "beta" : "latest"
304
+ });
305
+ }
306
+ if (!same) {
307
+ console.warn("[basic] New version available:", latestVersion, `
308
+ run "npm install @basictech/react@${latestVersion}" to update`);
309
+ }
310
+ if (usesBetaDistTag(currentVersion)) {
311
+ log("thank you for being on basictech/react beta :)");
312
+ }
313
+ return {
314
+ hasNewVersion: !same,
315
+ latestVersion,
316
+ currentVersion
317
+ };
318
+ } catch (error) {
319
+ log("Error checking for new version:", error);
320
+ return {
321
+ hasNewVersion: false,
322
+ latestVersion: null,
323
+ currentVersion: null
324
+ };
325
+ }
326
+ }
327
+ function cleanOAuthParamsFromUrl() {
328
+ if (window.location.search.includes("code") || window.location.search.includes("state")) {
329
+ const url = new URL(window.location.href);
330
+ url.searchParams.delete("code");
331
+ url.searchParams.delete("state");
332
+ window.history.replaceState({}, document.title, url.pathname + url.search);
333
+ log("Cleaned OAuth parameters from URL");
334
+ }
335
+ }
336
+ function getSyncStatus(statusCode) {
337
+ switch (statusCode) {
338
+ case -1:
339
+ return "ERROR";
340
+ case 0:
341
+ return "OFFLINE";
342
+ case 1:
343
+ return "CONNECTING";
344
+ case 2:
345
+ return "ONLINE";
346
+ case 3:
347
+ return "SYNCING";
348
+ case 4:
349
+ return "ERROR_WILL_RETRY";
350
+ default:
351
+ return "UNKNOWN";
352
+ }
353
+ }
354
+ var init_network = __esm({
355
+ "src/utils/network.ts"() {
356
+ "use strict";
357
+ init_config();
358
+ init_package();
359
+ }
360
+ });
361
+
362
+ // src/context.tsx
363
+ import { createContext, useContext } from "react";
364
+ function useBasic() {
365
+ return useContext(BasicContext);
366
+ }
367
+ var DBStatus, noDb, BasicContext;
368
+ var init_context = __esm({
369
+ "src/context.tsx"() {
370
+ "use strict";
371
+ DBStatus = /* @__PURE__ */ ((DBStatus2) => {
372
+ DBStatus2["LOADING"] = "LOADING";
373
+ DBStatus2["OFFLINE"] = "OFFLINE";
374
+ DBStatus2["CONNECTING"] = "CONNECTING";
375
+ DBStatus2["ONLINE"] = "ONLINE";
376
+ DBStatus2["SYNCING"] = "SYNCING";
377
+ DBStatus2["ERROR"] = "ERROR";
378
+ DBStatus2["ERROR_WILL_RETRY"] = "ERROR_WILL_RETRY";
379
+ DBStatus2["ERROR_TOKEN_EXPIRED"] = "ERROR_TOKEN_EXPIRED";
380
+ return DBStatus2;
381
+ })(DBStatus || {});
382
+ noDb = {
383
+ collection: () => {
384
+ throw new Error("no basicdb found - initialization failed. double check your schema.");
385
+ }
386
+ };
387
+ BasicContext = createContext({
388
+ isReady: false,
389
+ isSignedIn: false,
390
+ user: null,
391
+ did: null,
392
+ scope: null,
393
+ hasScope: () => false,
394
+ missingScopes: () => [],
395
+ signIn: () => Promise.resolve(),
396
+ signInWithHandle: () => Promise.resolve(),
397
+ signOut: () => Promise.resolve(),
398
+ signInWithCode: () => Promise.resolve({ success: false }),
399
+ getToken: (_options) => Promise.reject(new Error("no token")),
400
+ getSignInUrl: () => Promise.resolve(""),
401
+ db: noDb,
402
+ dbStatus: "LOADING" /* LOADING */,
403
+ dbMode: "sync",
404
+ devInfo: null,
405
+ refreshSchemaStatus: async () => {
406
+ },
407
+ isAuthReady: false,
408
+ signin: () => Promise.resolve(),
409
+ signout: () => Promise.resolve(),
410
+ signinWithCode: () => Promise.resolve({ success: false }),
411
+ getSignInLink: () => Promise.resolve("")
412
+ });
413
+ }
414
+ });
415
+
416
+ // src/dev/BasicDevToolbar.tsx
417
+ var BasicDevToolbar_exports = {};
418
+ __export(BasicDevToolbar_exports, {
419
+ BasicDevToolbar: () => BasicDevToolbar
420
+ });
421
+ import { useCallback, useMemo, useState } from "react";
422
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
423
+ function toneForAuth(isReady, isSignedIn) {
424
+ if (!isReady) return "muted";
425
+ if (isSignedIn) return "ok";
426
+ return "warn";
427
+ }
428
+ function toneForDb(dbMode, dbStatus) {
429
+ if (dbMode === "remote") return dbStatus === "ONLINE" /* ONLINE */ ? "ok" : "warn";
430
+ if (dbStatus === "ONLINE" /* ONLINE */ || dbStatus === "SYNCING" /* SYNCING */) return "ok";
431
+ if (dbStatus === "CONNECTING" /* CONNECTING */ || dbStatus === "LOADING" /* LOADING */) return "warn";
432
+ if (dbStatus === "OFFLINE" /* OFFLINE */) return "muted";
433
+ return "bad";
434
+ }
435
+ function toneForSchema(info) {
436
+ if (!info) return "muted";
437
+ if (info.valid && info.status === "current") return "ok";
438
+ if (info.status === "unpublished") return "warn";
439
+ if (info.status === "no_schema") return "muted";
440
+ return "bad";
441
+ }
442
+ function dbStatusLabel(status) {
443
+ switch (status) {
444
+ case "LOADING" /* LOADING */:
445
+ return "Initializing";
446
+ case "OFFLINE" /* OFFLINE */:
447
+ return "Offline";
448
+ case "CONNECTING" /* CONNECTING */:
449
+ return "Connecting";
450
+ case "ONLINE" /* ONLINE */:
451
+ return "Connected";
452
+ case "SYNCING" /* SYNCING */:
453
+ return "Syncing";
454
+ case "ERROR" /* ERROR */:
455
+ return "Error";
456
+ case "ERROR_WILL_RETRY" /* ERROR_WILL_RETRY */:
457
+ return "Retrying";
458
+ case "ERROR_TOKEN_EXPIRED" /* ERROR_TOKEN_EXPIRED */:
459
+ return "Token refresh";
460
+ default:
461
+ return String(status);
462
+ }
463
+ }
464
+ function chipColor(tone) {
465
+ switch (tone) {
466
+ case "ok":
467
+ return "#22c55e";
468
+ case "warn":
469
+ return "#eab308";
470
+ case "bad":
471
+ return "#ef4444";
472
+ default:
473
+ return "#71717a";
474
+ }
475
+ }
476
+ function displayDid(did) {
477
+ return did || "\u2014";
478
+ }
479
+ function displayUserLine(user) {
480
+ const parts = [];
481
+ if (user.sub) parts.push(`sub: ${user.sub}`);
482
+ if (user.email) parts.push(`email: ${user.email}`);
483
+ if (user.name) parts.push(`name: ${user.name}`);
484
+ return parts.length ? parts.join(" \xB7 ") : "\u2014";
485
+ }
486
+ function ClipboardIcon() {
487
+ return /* @__PURE__ */ jsxs(
488
+ "svg",
489
+ {
490
+ width: "14",
491
+ height: "14",
492
+ viewBox: "0 0 24 24",
493
+ fill: "none",
494
+ stroke: "currentColor",
495
+ strokeWidth: "2",
496
+ strokeLinecap: "round",
497
+ strokeLinejoin: "round",
498
+ "aria-hidden": true,
499
+ children: [
500
+ /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
501
+ /* @__PURE__ */ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
502
+ ]
503
+ }
504
+ );
505
+ }
506
+ function SectionHeader({ children }) {
507
+ return /* @__PURE__ */ jsx(
508
+ "div",
509
+ {
510
+ style: {
511
+ fontSize: 10,
512
+ fontWeight: 700,
513
+ color: "#e4e4e7",
514
+ letterSpacing: "0.07em",
515
+ textTransform: "uppercase",
516
+ marginBottom: 8
517
+ },
518
+ children
519
+ }
520
+ );
521
+ }
522
+ function SectionRule() {
523
+ const bleed = PANEL_PAD_X;
524
+ return /* @__PURE__ */ jsx(
525
+ "div",
526
+ {
527
+ role: "separator",
528
+ style: {
529
+ height: 1,
530
+ background: "rgba(255, 255, 255, 0.055)",
531
+ marginLeft: -bleed,
532
+ marginRight: -bleed,
533
+ marginTop: 14,
534
+ marginBottom: 10,
535
+ width: `calc(100% + ${bleed * 2}px)`
536
+ }
537
+ }
538
+ );
539
+ }
540
+ function CopyableRow({
541
+ rowKey,
542
+ label,
543
+ copyText,
544
+ copiedKey,
545
+ onCopied,
546
+ children
547
+ }) {
548
+ const [hover, setHover] = useState(false);
549
+ const canCopy = copyText.length > 0;
550
+ const handleClick = useCallback(
551
+ (e) => {
552
+ e.stopPropagation();
553
+ if (!canCopy) return;
554
+ void navigator.clipboard.writeText(copyText).then(() => onCopied(rowKey));
555
+ },
556
+ [canCopy, copyText, onCopied, rowKey]
557
+ );
558
+ return /* @__PURE__ */ jsxs(
559
+ "div",
560
+ {
561
+ role: canCopy ? "button" : void 0,
562
+ tabIndex: canCopy ? 0 : void 0,
563
+ onClick: canCopy ? handleClick : void 0,
564
+ onKeyDown: canCopy ? (e) => {
565
+ if (e.key === "Enter" || e.key === " ") {
566
+ e.preventDefault();
567
+ handleClick(e);
568
+ }
569
+ } : void 0,
570
+ onMouseEnter: () => setHover(true),
571
+ onMouseLeave: () => setHover(false),
572
+ style: {
573
+ display: "flex",
574
+ gap: 8,
575
+ marginBottom: 6,
576
+ alignItems: "flex-start",
577
+ borderRadius: 6,
578
+ padding: "4px 6px",
579
+ marginLeft: -6,
580
+ marginRight: -6,
581
+ cursor: canCopy ? "pointer" : "default",
582
+ background: hover && canCopy ? "rgba(255,255,255,0.06)" : "transparent",
583
+ transition: "background 0.12s ease"
584
+ },
585
+ children: [
586
+ /* @__PURE__ */ jsx("span", { style: { color: "#a1a1aa", minWidth: 88, flexShrink: 0, paddingTop: 2 }, children: label }),
587
+ /* @__PURE__ */ jsx(
588
+ "span",
589
+ {
590
+ style: {
591
+ flex: 1,
592
+ minWidth: 0,
593
+ wordBreak: "break-all",
594
+ paddingTop: 2,
595
+ lineHeight: 1.35
596
+ },
597
+ children
598
+ }
599
+ ),
600
+ canCopy && /* @__PURE__ */ jsx(
601
+ "span",
602
+ {
603
+ style: {
604
+ flexShrink: 0,
605
+ color: copiedKey === rowKey ? "#22c55e" : "#71717a",
606
+ opacity: hover || copiedKey === rowKey ? 1 : 0,
607
+ transition: "opacity 0.12s ease, color 0.12s ease",
608
+ paddingTop: 2,
609
+ display: "flex",
610
+ alignItems: "flex-start"
611
+ },
612
+ title: "Copy value",
613
+ children: copiedKey === rowKey ? /* @__PURE__ */ jsx("span", { style: { fontSize: 10 }, children: "\u2713" }) : /* @__PURE__ */ jsx(ClipboardIcon, {})
614
+ }
615
+ )
616
+ ]
617
+ }
618
+ );
619
+ }
620
+ function BasicDevToolbar({ enabled = true, debug }) {
621
+ const {
622
+ isReady,
623
+ isSignedIn,
624
+ user,
625
+ did,
626
+ scope,
627
+ missingScopes,
628
+ dbMode,
629
+ dbStatus,
630
+ devInfo,
631
+ refreshSchemaStatus
632
+ } = useBasic();
633
+ const [open, setOpen] = useState(false);
634
+ const [refreshing, setRefreshing] = useState(false);
635
+ const [copied, setCopied] = useState(false);
636
+ const [rowCopied, setRowCopied] = useState(null);
637
+ const show = enabled && typeof window !== "undefined" && isDevelopment(debug);
638
+ const authTone = toneForAuth(isReady, isSignedIn);
639
+ const dbTone = toneForDb(dbMode, dbStatus);
640
+ const schemaTone = toneForSchema(devInfo);
641
+ const syncTone = dbMode === "remote" ? "muted" : dbTone === "ok" || dbStatus === "SYNCING" /* SYNCING */ ? "ok" : dbTone === "warn" ? "warn" : dbTone === "bad" ? "bad" : "muted";
642
+ const handleRefreshSchema = useCallback(async () => {
643
+ setRefreshing(true);
644
+ try {
645
+ await refreshSchemaStatus();
646
+ } finally {
647
+ setRefreshing(false);
648
+ }
649
+ }, [refreshSchemaStatus]);
650
+ const missingList = missingScopes();
651
+ const debugPayload = useMemo(() => {
652
+ return {
653
+ sdkVersion: version,
654
+ isReady,
655
+ isSignedIn,
656
+ did: did ?? null,
657
+ user: user ? {
658
+ sub: user.sub,
659
+ email: user.email,
660
+ name: user.name,
661
+ picture: user.picture
662
+ } : null,
663
+ scope,
664
+ missingScopes: missingList,
665
+ dbMode,
666
+ dbStatus,
667
+ indexedDbName: dbMode === "sync" ? INDEXED_DB_NAME : null,
668
+ schema: devInfo
669
+ };
670
+ }, [isReady, isSignedIn, did, user, scope, dbMode, dbStatus, devInfo, missingList]);
671
+ const handleCopy = useCallback(async () => {
672
+ try {
673
+ await navigator.clipboard.writeText(JSON.stringify(debugPayload, null, 2));
674
+ setCopied(true);
675
+ setTimeout(() => setCopied(false), 2e3);
676
+ } catch {
677
+ }
678
+ }, [debugPayload]);
679
+ const onRowCopied = useCallback((key) => {
680
+ setRowCopied(key);
681
+ setTimeout(() => setRowCopied((k) => k === key ? null : k), 1500);
682
+ }, []);
683
+ if (!show) return null;
684
+ const shell = {
685
+ position: "fixed",
686
+ bottom: 12,
687
+ left: "50%",
688
+ transform: "translateX(-50%)",
689
+ zIndex: 99999,
690
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
691
+ fontSize: 11,
692
+ color: "#e4e4e7",
693
+ pointerEvents: "auto"
694
+ };
695
+ const bar = {
696
+ display: "flex",
697
+ alignItems: "center",
698
+ gap: 8,
699
+ padding: "8px 12px",
700
+ borderRadius: 999,
701
+ background: "rgba(24, 24, 27, 0.92)",
702
+ border: "1px solid rgba(63, 63, 70, 0.9)",
703
+ boxShadow: "0 8px 32px rgba(0,0,0,0.35)",
704
+ cursor: "pointer",
705
+ userSelect: "none"
706
+ };
707
+ const dot = (tone) => /* @__PURE__ */ jsx(
708
+ "span",
709
+ {
710
+ style: {
711
+ display: "block",
712
+ boxSizing: "border-box",
713
+ width: 6,
714
+ height: 6,
715
+ minWidth: 6,
716
+ minHeight: 6,
717
+ maxWidth: 6,
718
+ maxHeight: 6,
719
+ borderRadius: "50%",
720
+ background: chipColor(tone),
721
+ flexShrink: 0
722
+ }
723
+ }
724
+ );
725
+ const dotSlot = (title, tone) => /* @__PURE__ */ jsx(
726
+ "span",
727
+ {
728
+ title,
729
+ style: {
730
+ display: "inline-flex",
731
+ alignItems: "center",
732
+ justifyContent: "center",
733
+ width: 6,
734
+ height: 6,
735
+ flexShrink: 0,
736
+ lineHeight: 0
737
+ },
738
+ children: dot(tone)
739
+ }
740
+ );
741
+ const panel = {
742
+ marginBottom: 8,
743
+ maxHeight: "50vh",
744
+ overflow: "auto",
745
+ padding: PANEL_PAD_X,
746
+ borderRadius: 10,
747
+ background: "rgba(24, 24, 27, 0.96)",
748
+ border: "1px solid rgba(63, 63, 70, 0.9)",
749
+ boxShadow: "0 8px 32px rgba(0,0,0,0.35)",
750
+ minWidth: 300,
751
+ maxWidth: "min(560px, calc(100vw - 24px))"
752
+ };
753
+ const syncStatusText = dbStatusLabel(dbStatus);
754
+ return /* @__PURE__ */ jsxs("div", { style: shell, children: [
755
+ open && /* @__PURE__ */ jsxs("div", { style: panel, children: [
756
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: 12 }, children: [
757
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, fontSize: 12 }, children: "Basic SDK" }),
758
+ /* @__PURE__ */ jsxs("div", { style: { color: "#71717a", fontSize: 10, marginTop: 2 }, children: [
759
+ "v",
760
+ version
761
+ ] })
762
+ ] }),
763
+ /* @__PURE__ */ jsx(SectionHeader, { children: "Auth" }),
764
+ /* @__PURE__ */ jsx(
765
+ CopyableRow,
766
+ {
767
+ rowKey: "ready",
768
+ label: "Ready",
769
+ copyText: String(isReady),
770
+ copiedKey: rowCopied,
771
+ onCopied: onRowCopied,
772
+ children: String(isReady)
773
+ }
774
+ ),
775
+ /* @__PURE__ */ jsx(
776
+ CopyableRow,
777
+ {
778
+ rowKey: "signedIn",
779
+ label: "Signed in",
780
+ copyText: String(isSignedIn),
781
+ copiedKey: rowCopied,
782
+ onCopied: onRowCopied,
783
+ children: String(isSignedIn)
784
+ }
785
+ ),
786
+ /* @__PURE__ */ jsx(
787
+ CopyableRow,
788
+ {
789
+ rowKey: "did",
790
+ label: "DID",
791
+ copyText: did || "",
792
+ copiedKey: rowCopied,
793
+ onCopied: onRowCopied,
794
+ children: displayDid(did)
795
+ }
796
+ ),
797
+ /* @__PURE__ */ jsx(
798
+ CopyableRow,
799
+ {
800
+ rowKey: "user",
801
+ label: "User",
802
+ copyText: user ? displayUserLine(user) : "",
803
+ copiedKey: rowCopied,
804
+ onCopied: onRowCopied,
805
+ children: user ? displayUserLine(user) : "\u2014"
806
+ }
807
+ ),
808
+ /* @__PURE__ */ jsx(
809
+ CopyableRow,
810
+ {
811
+ rowKey: "scopes",
812
+ label: "Scopes",
813
+ copyText: scope || "",
814
+ copiedKey: rowCopied,
815
+ onCopied: onRowCopied,
816
+ children: scope || "\u2014"
817
+ }
818
+ ),
819
+ /* @__PURE__ */ jsx(
820
+ CopyableRow,
821
+ {
822
+ rowKey: "missingScopes",
823
+ label: "Missing scopes",
824
+ copyText: missingList.length ? missingList.join(", ") : "",
825
+ copiedKey: rowCopied,
826
+ onCopied: onRowCopied,
827
+ children: missingList.length ? missingList.join(", ") : "\u2014"
828
+ }
829
+ ),
830
+ /* @__PURE__ */ jsx(SectionRule, {}),
831
+ /* @__PURE__ */ jsx(SectionHeader, { children: "Database" }),
832
+ /* @__PURE__ */ jsx(
833
+ CopyableRow,
834
+ {
835
+ rowKey: "dbMode",
836
+ label: "Mode",
837
+ copyText: dbMode,
838
+ copiedKey: rowCopied,
839
+ onCopied: onRowCopied,
840
+ children: dbMode
841
+ }
842
+ ),
843
+ /* @__PURE__ */ jsx(
844
+ CopyableRow,
845
+ {
846
+ rowKey: "indexedDb",
847
+ label: "IndexedDB",
848
+ copyText: dbMode === "sync" ? INDEXED_DB_NAME : "",
849
+ copiedKey: rowCopied,
850
+ onCopied: onRowCopied,
851
+ children: dbMode === "sync" ? INDEXED_DB_NAME : "\u2014"
852
+ }
853
+ ),
854
+ /* @__PURE__ */ jsx(
855
+ CopyableRow,
856
+ {
857
+ rowKey: "syncStatus",
858
+ label: "Sync / status",
859
+ copyText: syncStatusText,
860
+ copiedKey: rowCopied,
861
+ onCopied: onRowCopied,
862
+ children: syncStatusText
863
+ }
864
+ ),
865
+ /* @__PURE__ */ jsx(SectionRule, {}),
866
+ /* @__PURE__ */ jsx(SectionHeader, { children: "Schema" }),
867
+ devInfo ? /* @__PURE__ */ jsxs(Fragment, { children: [
868
+ /* @__PURE__ */ jsx(
869
+ CopyableRow,
870
+ {
871
+ rowKey: "schemaProject",
872
+ label: "Project",
873
+ copyText: devInfo.projectId ?? "",
874
+ copiedKey: rowCopied,
875
+ onCopied: onRowCopied,
876
+ children: devInfo.projectId ?? "\u2014"
877
+ }
878
+ ),
879
+ /* @__PURE__ */ jsx(
880
+ CopyableRow,
881
+ {
882
+ rowKey: "schemaLocalVer",
883
+ label: "Local version",
884
+ copyText: devInfo.localVersion !== void 0 && devInfo.localVersion !== null ? String(devInfo.localVersion) : "",
885
+ copiedKey: rowCopied,
886
+ onCopied: onRowCopied,
887
+ children: devInfo.localVersion ?? "\u2014"
888
+ }
889
+ ),
890
+ /* @__PURE__ */ jsx(
891
+ CopyableRow,
892
+ {
893
+ rowKey: "schemaRemote",
894
+ label: "Remote check",
895
+ copyText: devInfo.status,
896
+ copiedKey: rowCopied,
897
+ onCopied: onRowCopied,
898
+ children: devInfo.status
899
+ }
900
+ ),
901
+ /* @__PURE__ */ jsx(
902
+ CopyableRow,
903
+ {
904
+ rowKey: "schemaValid",
905
+ label: "Valid",
906
+ copyText: String(devInfo.valid),
907
+ copiedKey: rowCopied,
908
+ onCopied: onRowCopied,
909
+ children: String(devInfo.valid)
910
+ }
911
+ ),
912
+ /* @__PURE__ */ jsx(
913
+ CopyableRow,
914
+ {
915
+ rowKey: "schemaChecked",
916
+ label: "Checked",
917
+ copyText: devInfo.lastCheckedAt ? new Date(devInfo.lastCheckedAt).toISOString() : "",
918
+ copiedKey: rowCopied,
919
+ onCopied: onRowCopied,
920
+ children: devInfo.lastCheckedAt ? new Date(devInfo.lastCheckedAt).toLocaleString() : "\u2014"
921
+ }
922
+ ),
923
+ devInfo.error ? /* @__PURE__ */ jsx(
924
+ CopyableRow,
925
+ {
926
+ rowKey: "schemaError",
927
+ label: "Error",
928
+ copyText: devInfo.error,
929
+ copiedKey: rowCopied,
930
+ onCopied: onRowCopied,
931
+ children: devInfo.error
932
+ }
933
+ ) : null
934
+ ] }) : /* @__PURE__ */ jsx(
935
+ CopyableRow,
936
+ {
937
+ rowKey: "schemaStatus",
938
+ label: "Status",
939
+ copyText: "No schema on provider",
940
+ copiedKey: rowCopied,
941
+ onCopied: onRowCopied,
942
+ children: "No schema on provider"
943
+ }
944
+ ),
945
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, marginTop: 12, flexWrap: "wrap" }, children: [
946
+ /* @__PURE__ */ jsx(
947
+ "button",
948
+ {
949
+ type: "button",
950
+ onClick: (e) => {
951
+ e.stopPropagation();
952
+ void handleRefreshSchema();
953
+ },
954
+ disabled: refreshing,
955
+ style: {
956
+ padding: "6px 10px",
957
+ borderRadius: 6,
958
+ border: "1px solid #3f3f46",
959
+ background: "#27272a",
960
+ color: "#e4e4e7",
961
+ cursor: refreshing ? "wait" : "pointer",
962
+ fontSize: 11,
963
+ fontFamily: "inherit"
964
+ },
965
+ children: refreshing ? "Refreshing\u2026" : "Refresh schema"
966
+ }
967
+ ),
968
+ /* @__PURE__ */ jsx(
969
+ "button",
970
+ {
971
+ type: "button",
972
+ onClick: (e) => {
973
+ e.stopPropagation();
974
+ void handleCopy();
975
+ },
976
+ style: {
977
+ padding: "6px 10px",
978
+ borderRadius: 6,
979
+ border: "1px solid #3f3f46",
980
+ background: "#27272a",
981
+ color: "#e4e4e7",
982
+ cursor: "pointer",
983
+ fontSize: 11,
984
+ fontFamily: "inherit"
985
+ },
986
+ children: copied ? "Copied" : "Copy debug info"
987
+ }
988
+ )
989
+ ] })
990
+ ] }),
991
+ /* @__PURE__ */ jsxs(
992
+ "button",
993
+ {
994
+ type: "button",
995
+ "aria-expanded": open,
996
+ onClick: () => setOpen((o) => !o),
997
+ style: {
998
+ ...bar,
999
+ border: "none",
1000
+ width: "100%",
1001
+ cursor: "pointer"
1002
+ },
1003
+ children: [
1004
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, letterSpacing: 0.02 }, children: "Basic" }),
1005
+ /* @__PURE__ */ jsxs(
1006
+ "span",
1007
+ {
1008
+ style: {
1009
+ display: "inline-flex",
1010
+ alignItems: "center",
1011
+ gap: 6,
1012
+ marginLeft: 8,
1013
+ height: 6,
1014
+ flexShrink: 0,
1015
+ lineHeight: 0
1016
+ },
1017
+ children: [
1018
+ dotSlot("Auth", authTone),
1019
+ dotSlot("DB", dbTone),
1020
+ dotSlot("Sync", syncTone),
1021
+ dotSlot("Schema", schemaTone)
1022
+ ]
1023
+ }
1024
+ ),
1025
+ /* @__PURE__ */ jsx("span", { style: { color: "#71717a", marginLeft: 4 }, children: open ? "\u25BE" : "\u25B4" })
1026
+ ]
1027
+ }
1028
+ )
1029
+ ] });
1030
+ }
1031
+ var INDEXED_DB_NAME, PANEL_PAD_X;
1032
+ var init_BasicDevToolbar = __esm({
1033
+ "src/dev/BasicDevToolbar.tsx"() {
1034
+ "use strict";
1035
+ "use client";
1036
+ init_context();
1037
+ init_package();
1038
+ init_network();
1039
+ INDEXED_DB_NAME = "basicdb";
1040
+ PANEL_PAD_X = 12;
1041
+ }
1042
+ });
1043
+
219
1044
  // src/AuthContext.tsx
220
- import { createContext, useContext, useEffect, useState, useRef } from "react";
1045
+ import { useCallback as useCallback2, useEffect, useRef, useState as useState2, Suspense, lazy } from "react";
221
1046
 
222
1047
  // src/sync/index.ts
223
1048
  init_config();
@@ -808,75 +1633,8 @@ async function resolveHandle(handle) {
808
1633
  return resolved;
809
1634
  }
810
1635
 
811
- // src/utils/network.ts
812
- init_config();
813
-
814
- // package.json
815
- var version = "0.8.0-beta.1";
816
-
817
- // src/utils/network.ts
818
- function isDevelopment(debug) {
819
- 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;
820
- }
821
- async function checkForNewVersion() {
822
- try {
823
- const isBeta = version.includes("beta");
824
- const response = await fetch(`https://registry.npmjs.org/@basictech/react/${isBeta ? "beta" : "latest"}`);
825
- if (!response.ok) {
826
- throw new Error("Failed to fetch version from npm");
827
- }
828
- const data = await response.json();
829
- const latestVersion = data.version;
830
- if (latestVersion !== version) {
831
- console.warn("[basic] New version available:", latestVersion, `
832
- run "npm install @basictech/react@${latestVersion}" to update`);
833
- }
834
- if (isBeta) {
835
- log("thank you for being on basictech/react beta :)");
836
- }
837
- return {
838
- hasNewVersion: version !== latestVersion,
839
- latestVersion,
840
- currentVersion: version
841
- };
842
- } catch (error) {
843
- log("Error checking for new version:", error);
844
- return {
845
- hasNewVersion: false,
846
- latestVersion: null,
847
- currentVersion: null
848
- };
849
- }
850
- }
851
- function cleanOAuthParamsFromUrl() {
852
- if (window.location.search.includes("code") || window.location.search.includes("state")) {
853
- const url = new URL(window.location.href);
854
- url.searchParams.delete("code");
855
- url.searchParams.delete("state");
856
- window.history.pushState({}, document.title, url.pathname + url.search);
857
- log("Cleaned OAuth parameters from URL");
858
- }
859
- }
860
- function getSyncStatus(statusCode) {
861
- switch (statusCode) {
862
- case -1:
863
- return "ERROR";
864
- case 0:
865
- return "OFFLINE";
866
- case 1:
867
- return "CONNECTING";
868
- case 2:
869
- return "ONLINE";
870
- case 3:
871
- return "SYNCING";
872
- case 4:
873
- return "ERROR_WILL_RETRY";
874
- default:
875
- return "UNKNOWN";
876
- }
877
- }
878
-
879
1636
  // src/core/auth/AuthManager.ts
1637
+ init_network();
880
1638
  init_config();
881
1639
  function generateCodeVerifier() {
882
1640
  const array = new Uint8Array(32);
@@ -1024,8 +1782,11 @@ var AuthManager = class {
1024
1782
  const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1025
1783
  if (refreshToken) {
1026
1784
  log("Found refresh token in storage, attempting to refresh access token");
1027
- this.exchangeToken(refreshToken, true).catch((error) => {
1785
+ this.exchangeToken(refreshToken, true).catch(async (error) => {
1028
1786
  log("Error fetching refresh token:", error);
1787
+ if (this.isNetworkError(error)) {
1788
+ await this.restoreCachedUser();
1789
+ }
1029
1790
  });
1030
1791
  } else {
1031
1792
  const cachedUserInfo = await this.storage.get(STORAGE_KEYS.USER_INFO);
@@ -1094,7 +1855,8 @@ var AuthManager = class {
1094
1855
  log("Token refresh already in progress, waiting...");
1095
1856
  try {
1096
1857
  const newToken = await this.refreshPromise;
1097
- return newToken?.access_token || "";
1858
+ if (!newToken?.access_token) throw new Error("Token refresh returned empty access token");
1859
+ return newToken.access_token;
1098
1860
  } catch (error) {
1099
1861
  log("In-flight refresh failed:", error);
1100
1862
  if (this.isNetworkError(error)) {
@@ -1108,7 +1870,8 @@ var AuthManager = class {
1108
1870
  if (refreshToken) {
1109
1871
  try {
1110
1872
  const newToken = await this.exchangeToken(refreshToken, true);
1111
- return newToken?.access_token || "";
1873
+ if (!newToken?.access_token) throw new Error("Token refresh returned empty access token");
1874
+ return newToken.access_token;
1112
1875
  } catch (error) {
1113
1876
  log("Failed to refresh expired token:", error);
1114
1877
  if (this.isNetworkError(error)) {
@@ -1121,7 +1884,8 @@ var AuthManager = class {
1121
1884
  throw new Error("no refresh token available");
1122
1885
  }
1123
1886
  }
1124
- return this.token.access_token || "";
1887
+ if (!this.token.access_token) throw new Error("Token exists but access_token is empty");
1888
+ return this.token.access_token;
1125
1889
  }
1126
1890
  async getSignInUrl(redirectUri, endpoints) {
1127
1891
  log("getting sign in link...");
@@ -1130,7 +1894,7 @@ var AuthManager = class {
1130
1894
  }
1131
1895
  const pdsEndpoints = endpoints || this.defaultPdsEndpoints();
1132
1896
  await this.storage.set(STORAGE_KEYS.PDS_ENDPOINTS, JSON.stringify(pdsEndpoints));
1133
- const randomState = Math.random().toString(36).substring(6);
1897
+ const randomState = base64UrlEncode(crypto.getRandomValues(new Uint8Array(16)));
1134
1898
  await this.storage.set(STORAGE_KEYS.AUTH_STATE, randomState);
1135
1899
  const redirectUrl = redirectUri || window.location.href;
1136
1900
  if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
@@ -1249,7 +2013,10 @@ var AuthManager = class {
1249
2013
  return requested.filter((s) => !granted.has(s));
1250
2014
  }
1251
2015
  /**
1252
- * Register online/offline handlers that retry pending refreshes.
2016
+ * Register online/offline and visibility handlers that retry pending
2017
+ * refreshes and proactively refresh tokens when the app resumes from
2018
+ * background (critical for PWAs and mobile browsers where timers are
2019
+ * frozen while backgrounded).
1253
2020
  * Returns a cleanup function for useEffect teardown.
1254
2021
  */
1255
2022
  setupNetworkListeners() {
@@ -1271,11 +2038,25 @@ var AuthManager = class {
1271
2038
  log("Network went offline");
1272
2039
  this.isOnline = false;
1273
2040
  };
2041
+ const handleVisibilityChange = () => {
2042
+ if (document.visibilityState === "visible" && this.isSignedIn) {
2043
+ log("App became visible - checking token freshness");
2044
+ this.getToken().catch((err) => {
2045
+ log("Token refresh on visibility resume failed:", err);
2046
+ });
2047
+ }
2048
+ };
1274
2049
  window.addEventListener("online", handleOnline);
1275
2050
  window.addEventListener("offline", handleOffline);
2051
+ if (typeof document !== "undefined") {
2052
+ document.addEventListener("visibilitychange", handleVisibilityChange);
2053
+ }
1276
2054
  return () => {
1277
2055
  window.removeEventListener("online", handleOnline);
1278
2056
  window.removeEventListener("offline", handleOffline);
2057
+ if (typeof document !== "undefined") {
2058
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
2059
+ }
1279
2060
  };
1280
2061
  }
1281
2062
  // ------------------------------------------------------------------
@@ -1338,39 +2119,26 @@ var AuthManager = class {
1338
2119
  const decoded = jwtDecode(this.token.access_token);
1339
2120
  if (decoded.sub) this.did = decoded.sub;
1340
2121
  if (decoded.scope) this.tokenScope = decoded.scope;
1341
- const expirationBuffer = 5;
1342
- const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
1343
- if (isExpired) {
1344
- log("token is expired - refreshing ...");
1345
- const refreshToken = this.token.refresh_token;
1346
- if (!refreshToken) {
1347
- log("Error: No refresh token available for expired token");
1348
- this.isAuthReady = true;
1349
- this.notify();
1350
- return;
1351
- }
1352
- try {
1353
- const newToken = await this.exchangeToken(refreshToken, true);
1354
- await this.fetchUser(newToken?.access_token || "");
1355
- } catch (error) {
1356
- log("Failed to refresh token in processNewToken:", error);
1357
- if (this.isNetworkError(error)) {
1358
- log("Network issue - continuing with expired token until online");
1359
- await this.fetchUser(this.token.access_token);
1360
- } else {
1361
- this.isAuthReady = true;
1362
- this.notify();
1363
- }
1364
- }
1365
- } else {
1366
- await this.fetchUser(this.token.access_token);
1367
- }
2122
+ await this.fetchUser(this.token.access_token);
1368
2123
  } catch (error) {
1369
2124
  log("Error processing token:", error);
1370
2125
  this.isAuthReady = true;
1371
2126
  this.notify();
1372
2127
  }
1373
2128
  }
2129
+ async restoreCachedUser() {
2130
+ const cached = await this.storage.get(STORAGE_KEYS.USER_INFO);
2131
+ if (cached) {
2132
+ try {
2133
+ this.user = JSON.parse(cached);
2134
+ this.isSignedIn = true;
2135
+ log("Restored cached user info for offline mode");
2136
+ } catch {
2137
+ }
2138
+ }
2139
+ this.isAuthReady = true;
2140
+ this.notify();
2141
+ }
1374
2142
  async fetchUser(accessToken) {
1375
2143
  log("fetching user");
1376
2144
  try {
@@ -1404,8 +2172,12 @@ var AuthManager = class {
1404
2172
  this.notify();
1405
2173
  } catch (error) {
1406
2174
  log("Failed to fetch user info:", error);
1407
- this.isAuthReady = true;
1408
- this.notify();
2175
+ if (this.isNetworkError(error)) {
2176
+ await this.restoreCachedUser();
2177
+ } else {
2178
+ this.isAuthReady = true;
2179
+ this.notify();
2180
+ }
1409
2181
  }
1410
2182
  }
1411
2183
  /**
@@ -1501,9 +2273,12 @@ var AuthManager = class {
1501
2273
  this.pendingRefresh = true;
1502
2274
  throw new Error("Network issue - refresh will be retried when online");
1503
2275
  }
1504
- await this.clearStoredAuth();
1505
- this.resetAuthState();
1506
- this.notify();
2276
+ const definitiveErrors = ["invalid_grant", "invalid_client", "unauthorized_client"];
2277
+ if (typeof token.error === "string" && definitiveErrors.includes(token.error)) {
2278
+ await this.clearStoredAuth();
2279
+ this.resetAuthState();
2280
+ this.notify();
2281
+ }
1507
2282
  throw new Error(`Token refresh failed: ${token.error}`);
1508
2283
  } else {
1509
2284
  this.token = token;
@@ -1524,7 +2299,9 @@ var AuthManager = class {
1524
2299
  return token;
1525
2300
  } catch (error) {
1526
2301
  log("Token refresh error:", error);
1527
- if (!this.isNetworkError(error)) {
2302
+ const msg = error instanceof Error ? error.message : "";
2303
+ const alreadyHandled = msg.startsWith("Token refresh failed:");
2304
+ if (!alreadyHandled && !this.isNetworkError(error)) {
1528
2305
  await this.clearStoredAuth();
1529
2306
  this.resetAuthState();
1530
2307
  this.notify();
@@ -1568,6 +2345,7 @@ var AuthManager = class {
1568
2345
  await this.storage.remove(STORAGE_KEYS.PDS_ENDPOINTS);
1569
2346
  }
1570
2347
  isNetworkError(error) {
2348
+ if (error instanceof TypeError) return true;
1571
2349
  if (error instanceof Error) {
1572
2350
  return error.message.includes("offline") || error.message.includes("Network");
1573
2351
  }
@@ -1577,6 +2355,7 @@ var AuthManager = class {
1577
2355
 
1578
2356
  // src/AuthContext.tsx
1579
2357
  init_config();
2358
+ init_package();
1580
2359
 
1581
2360
  // src/updater/versionUpdater.ts
1582
2361
  init_config();
@@ -1701,6 +2480,9 @@ function getMigrations() {
1701
2480
  ];
1702
2481
  }
1703
2482
 
2483
+ // src/AuthContext.tsx
2484
+ init_network();
2485
+
1704
2486
  // src/utils/schema.ts
1705
2487
  init_config();
1706
2488
  import { validateSchema, compareSchemas } from "@basictech/schema";
@@ -1800,57 +2582,18 @@ async function validateAndCheckSchema(schema) {
1800
2582
  }
1801
2583
 
1802
2584
  // src/AuthContext.tsx
1803
- import { jsx, jsxs } from "react/jsx-runtime";
2585
+ init_context();
2586
+ init_context();
2587
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2588
+ var BasicDevToolbar2 = lazy(
2589
+ () => Promise.resolve().then(() => (init_BasicDevToolbar(), BasicDevToolbar_exports)).then((m) => ({ default: m.BasicDevToolbar }))
2590
+ );
1804
2591
  var DEFAULT_AUTH_CONFIG = {
1805
2592
  scopes: "profile,email,app:admin",
1806
2593
  pds_url: "https://pds.basic.id",
1807
2594
  admin_url: "https://api.basic.tech",
1808
2595
  ws_url: "wss://pds.basic.id/ws"
1809
2596
  };
1810
- var DBStatus = /* @__PURE__ */ ((DBStatus2) => {
1811
- DBStatus2["LOADING"] = "LOADING";
1812
- DBStatus2["OFFLINE"] = "OFFLINE";
1813
- DBStatus2["CONNECTING"] = "CONNECTING";
1814
- DBStatus2["ONLINE"] = "ONLINE";
1815
- DBStatus2["SYNCING"] = "SYNCING";
1816
- DBStatus2["ERROR"] = "ERROR";
1817
- DBStatus2["ERROR_WILL_RETRY"] = "ERROR_WILL_RETRY";
1818
- DBStatus2["ERROR_TOKEN_EXPIRED"] = "ERROR_TOKEN_EXPIRED";
1819
- return DBStatus2;
1820
- })(DBStatus || {});
1821
- var noDb = {
1822
- collection: () => {
1823
- throw new Error("no basicdb found - initialization failed. double check your schema.");
1824
- }
1825
- };
1826
- var BasicContext = createContext({
1827
- // Auth state
1828
- isReady: false,
1829
- isSignedIn: false,
1830
- user: null,
1831
- did: null,
1832
- scope: null,
1833
- hasScope: () => false,
1834
- missingScopes: () => [],
1835
- // Auth actions
1836
- signIn: () => Promise.resolve(),
1837
- signInWithHandle: () => Promise.resolve(),
1838
- signOut: () => Promise.resolve(),
1839
- signInWithCode: () => Promise.resolve({ success: false }),
1840
- // Token management
1841
- getToken: (_options) => Promise.reject(new Error("no token")),
1842
- getSignInUrl: () => Promise.resolve(""),
1843
- // DB access
1844
- db: noDb,
1845
- dbStatus: "LOADING" /* LOADING */,
1846
- dbMode: "sync",
1847
- // Legacy aliases
1848
- isAuthReady: false,
1849
- signin: () => Promise.resolve(),
1850
- signout: () => Promise.resolve(),
1851
- signinWithCode: () => Promise.resolve({ success: false }),
1852
- getSignInLink: () => Promise.resolve("")
1853
- });
1854
2597
  function snapshotAuth(mgr) {
1855
2598
  return {
1856
2599
  isSignedIn: mgr.isSignedIn,
@@ -1868,7 +2611,8 @@ function BasicProvider({
1868
2611
  debug = false,
1869
2612
  storage,
1870
2613
  auth,
1871
- dbMode = "sync"
2614
+ dbMode = "sync",
2615
+ devToolbar = false
1872
2616
  }) {
1873
2617
  const project_id = schema?.project_id || project_id_prop;
1874
2618
  if (auth?.server_url && !auth?.pds_url) {
@@ -1883,7 +2627,9 @@ function BasicProvider({
1883
2627
  const scopesString = Array.isArray(authConfig.scopes) ? authConfig.scopes.join(" ") : authConfig.scopes;
1884
2628
  const storageRef = useRef(storage || new LocalStorageAdapter());
1885
2629
  const storageAdapter = storageRef.current;
1886
- const [authState, setAuthState] = useState({
2630
+ const schemaRef = useRef(schema);
2631
+ schemaRef.current = schema;
2632
+ const [authState, setAuthState] = useState2({
1887
2633
  isSignedIn: false,
1888
2634
  hasToken: false,
1889
2635
  isAuthReady: false,
@@ -1907,11 +2653,47 @@ function BasicProvider({
1907
2653
  }
1908
2654
  const syncRef = useRef(null);
1909
2655
  const remoteDbRef = useRef(null);
1910
- const [shouldConnect, setShouldConnect] = useState(false);
1911
- const [dbStatus, setDbStatus] = useState("OFFLINE" /* OFFLINE */);
1912
- const [isReady, setIsReady] = useState(false);
1913
- const [error, setError] = useState(null);
2656
+ const [shouldConnect, setShouldConnect] = useState2(false);
2657
+ const [dbStatus, setDbStatus] = useState2("OFFLINE" /* OFFLINE */);
2658
+ const [isReady, setIsReady] = useState2(false);
2659
+ const [error, setError] = useState2(null);
2660
+ const [schemaDevInfo, setSchemaDevInfo] = useState2(null);
1914
2661
  const isDevMode = () => isDevelopment(debug);
2662
+ const refreshSchemaStatus = useCallback2(async () => {
2663
+ const s = schemaRef.current;
2664
+ if (!s) {
2665
+ setSchemaDevInfo(
2666
+ project_id ? {
2667
+ projectId: project_id,
2668
+ localVersion: void 0,
2669
+ status: "no_schema",
2670
+ valid: false,
2671
+ lastCheckedAt: Date.now()
2672
+ } : null
2673
+ );
2674
+ return;
2675
+ }
2676
+ const result = await validateAndCheckSchema(s);
2677
+ if (!result.isValid) {
2678
+ const errText = result.errors?.map((e) => e.message || "").join("; ") || "invalid";
2679
+ setSchemaDevInfo({
2680
+ projectId: s.project_id ?? null,
2681
+ localVersion: s.version,
2682
+ status: "invalid",
2683
+ valid: false,
2684
+ lastCheckedAt: Date.now(),
2685
+ error: errText
2686
+ });
2687
+ return;
2688
+ }
2689
+ setSchemaDevInfo({
2690
+ projectId: s.project_id ?? null,
2691
+ localVersion: s.version,
2692
+ status: result.schemaStatus.status ?? "unknown",
2693
+ valid: result.schemaStatus.valid,
2694
+ lastCheckedAt: Date.now()
2695
+ });
2696
+ }, [project_id]);
1915
2697
  useEffect(() => {
1916
2698
  const runVersionUpdater = async () => {
1917
2699
  try {
@@ -1973,6 +2755,10 @@ function BasicProvider({
1973
2755
  debug,
1974
2756
  onAuthError: (error2) => {
1975
2757
  log("RemoteDB auth error:", error2);
2758
+ if (error2.errorType === "forbidden") {
2759
+ log("403 Forbidden - user lacks required scope, not signing out");
2760
+ return;
2761
+ }
1976
2762
  handleSignOut();
1977
2763
  }
1978
2764
  });
@@ -1985,11 +2771,19 @@ function BasicProvider({
1985
2771
  if (!result.isValid) {
1986
2772
  let errorMessage = "";
1987
2773
  if (result.errors) {
1988
- result.errors.forEach((error2, index) => {
1989
- errorMessage += `${index + 1}: ${error2.message} - at ${error2.instancePath}
2774
+ result.errors.forEach((err, index) => {
2775
+ errorMessage += `${index + 1}: ${err.message} - at ${err.instancePath}
1990
2776
  `;
1991
2777
  });
1992
2778
  }
2779
+ setSchemaDevInfo({
2780
+ projectId: schema?.project_id ?? null,
2781
+ localVersion: schema?.version,
2782
+ status: "invalid",
2783
+ valid: false,
2784
+ lastCheckedAt: Date.now(),
2785
+ error: errorMessage.trim() || void 0
2786
+ });
1993
2787
  setError({
1994
2788
  code: "schema_invalid",
1995
2789
  title: "Basic Schema is invalid!",
@@ -1998,6 +2792,13 @@ function BasicProvider({
1998
2792
  setIsReady(true);
1999
2793
  return null;
2000
2794
  }
2795
+ setSchemaDevInfo({
2796
+ projectId: schema?.project_id ?? null,
2797
+ localVersion: schema?.version,
2798
+ status: result.schemaStatus.status ?? "unknown",
2799
+ valid: result.schemaStatus.valid,
2800
+ lastCheckedAt: Date.now()
2801
+ });
2001
2802
  if (dbMode === "remote") {
2002
2803
  initRemoteDb();
2003
2804
  } else {
@@ -2017,6 +2818,15 @@ function BasicProvider({
2017
2818
  if (schema) {
2018
2819
  checkSchema();
2019
2820
  } else {
2821
+ setSchemaDevInfo(
2822
+ project_id ? {
2823
+ projectId: project_id,
2824
+ localVersion: void 0,
2825
+ status: "no_schema",
2826
+ valid: false,
2827
+ lastCheckedAt: Date.now()
2828
+ } : null
2829
+ );
2020
2830
  if (dbMode === "remote" && project_id) {
2021
2831
  initRemoteDb();
2022
2832
  } else {
@@ -2083,68 +2893,71 @@ function BasicProvider({
2083
2893
  return syncRef.current || noDb;
2084
2894
  };
2085
2895
  const contextValue = {
2086
- // Auth state
2087
2896
  isReady: authState.isAuthReady,
2088
2897
  isSignedIn: authState.isSignedIn,
2089
2898
  user: authState.user,
2090
2899
  did: authState.did,
2091
2900
  scope: authState.tokenScope,
2092
- hasScope: (scope) => authRef.current.hasScope(scope),
2901
+ hasScope: (s) => authRef.current.hasScope(s),
2093
2902
  missingScopes: () => authRef.current.missingScopes(),
2094
- // Auth actions
2095
2903
  signIn: handleSignIn,
2096
2904
  signInWithHandle: handleSignInWithHandle,
2097
2905
  signOut: handleSignOut,
2098
2906
  signInWithCode: (code, state) => authRef.current.signInWithCode(code, state),
2099
- // Token management
2100
2907
  getToken: (opts) => authRef.current.getToken(opts),
2101
2908
  getSignInUrl: (redirectUri) => authRef.current.getSignInUrl(redirectUri),
2102
- // DB access
2103
2909
  db: getCurrentDb(),
2104
2910
  dbStatus,
2105
2911
  dbMode,
2106
- // Legacy aliases (deprecated)
2912
+ devInfo: schemaDevInfo,
2913
+ refreshSchemaStatus,
2107
2914
  isAuthReady: authState.isAuthReady,
2108
2915
  signin: handleSignIn,
2109
2916
  signout: handleSignOut,
2110
2917
  signinWithCode: (code, state) => authRef.current.signInWithCode(code, state),
2111
2918
  getSignInLink: (redirectUri) => authRef.current.getSignInUrl(redirectUri)
2112
2919
  };
2113
- return /* @__PURE__ */ jsxs(BasicContext.Provider, { value: contextValue, children: [
2114
- error && isDevMode() && /* @__PURE__ */ jsx(ErrorDisplay, { error }),
2920
+ return /* @__PURE__ */ jsxs2(BasicContext.Provider, { value: contextValue, children: [
2921
+ error && isDevMode() && /* @__PURE__ */ jsx2(ErrorDisplay, { error }),
2922
+ devToolbar && isDevMode() && /* @__PURE__ */ jsx2(Suspense, { fallback: null, children: /* @__PURE__ */ jsx2(BasicDevToolbar2, { debug }) }),
2115
2923
  isReady && children
2116
2924
  ] });
2117
2925
  }
2118
2926
  function ErrorDisplay({ error }) {
2119
- return /* @__PURE__ */ jsxs("div", { style: {
2120
- position: "absolute",
2121
- top: 20,
2122
- left: 20,
2123
- color: "black",
2124
- backgroundColor: "#f8d7da",
2125
- border: "1px solid #f5c6cb",
2126
- borderRadius: "4px",
2127
- padding: "20px",
2128
- maxWidth: "400px",
2129
- margin: "20px auto",
2130
- boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
2131
- fontFamily: "monospace"
2132
- }, children: [
2133
- /* @__PURE__ */ jsxs("h3", { style: { fontSize: "0.8rem", opacity: 0.8 }, children: [
2134
- "code: ",
2135
- error.code
2136
- ] }),
2137
- /* @__PURE__ */ jsx("h1", { style: { fontSize: "1.2rem", lineHeight: "1.5" }, children: error.title }),
2138
- /* @__PURE__ */ jsx("p", { children: error.message })
2139
- ] });
2140
- }
2141
- function useBasic() {
2142
- return useContext(BasicContext);
2927
+ return /* @__PURE__ */ jsxs2(
2928
+ "div",
2929
+ {
2930
+ style: {
2931
+ position: "absolute",
2932
+ top: 20,
2933
+ left: 20,
2934
+ color: "black",
2935
+ backgroundColor: "#f8d7da",
2936
+ border: "1px solid #f5c6cb",
2937
+ borderRadius: "4px",
2938
+ padding: "20px",
2939
+ maxWidth: "400px",
2940
+ margin: "20px auto",
2941
+ boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
2942
+ fontFamily: "monospace"
2943
+ },
2944
+ children: [
2945
+ /* @__PURE__ */ jsxs2("h3", { style: { fontSize: "0.8rem", opacity: 0.8 }, children: [
2946
+ "code: ",
2947
+ error.code
2948
+ ] }),
2949
+ /* @__PURE__ */ jsx2("h1", { style: { fontSize: "1.2rem", lineHeight: 1.5 }, children: error.title }),
2950
+ /* @__PURE__ */ jsx2("p", { children: error.message })
2951
+ ]
2952
+ }
2953
+ );
2143
2954
  }
2144
2955
 
2145
2956
  // src/index.ts
2957
+ init_BasicDevToolbar();
2146
2958
  import { useLiveQuery as useQuery } from "dexie-react-hooks";
2147
2959
  export {
2960
+ BasicDevToolbar,
2148
2961
  BasicProvider,
2149
2962
  DBStatus,
2150
2963
  NotAuthenticatedError,