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