@basictech/react 0.7.0 → 0.8.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -24,25 +24,53 @@ var init_config = __esm({
24
24
  }
25
25
  });
26
26
 
27
+ // src/sync/tokenRegistry.ts
28
+ function setTokenGetter(url, fn) {
29
+ registry.set(url, fn);
30
+ }
31
+ function getTokenGetter(url) {
32
+ return registry.get(url);
33
+ }
34
+ var registry;
35
+ var init_tokenRegistry = __esm({
36
+ "src/sync/tokenRegistry.ts"() {
37
+ "use strict";
38
+ registry = /* @__PURE__ */ new Map();
39
+ }
40
+ });
41
+
27
42
  // src/sync/syncProtocol.js
28
43
  var syncProtocol_exports = {};
29
44
  __export(syncProtocol_exports, {
30
45
  syncProtocol: () => syncProtocol
31
46
  });
32
47
  import { Dexie } from "dexie";
48
+ function decodeJwtExp(token) {
49
+ try {
50
+ var parts = token.split(".");
51
+ if (parts.length !== 3) return null;
52
+ var payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
53
+ return typeof payload.exp === "number" ? payload.exp : null;
54
+ } catch (_) {
55
+ return null;
56
+ }
57
+ }
33
58
  var syncProtocol;
34
59
  var init_syncProtocol = __esm({
35
60
  "src/sync/syncProtocol.js"() {
36
61
  "use strict";
37
62
  "use client";
38
63
  init_config();
64
+ init_tokenRegistry();
39
65
  syncProtocol = function() {
40
66
  log("Initializing syncProtocol");
41
67
  var RECONNECT_DELAY = 5e3;
68
+ var TOKEN_REFRESH_BUFFER = 60;
42
69
  Dexie.Syncable.registerSyncProtocol("websocket", {
43
70
  sync: function(context, url, options, baseRevision, syncedRevision, changes, partial, applyRemoteChanges, onChangesAccepted, onSuccess, onError) {
44
71
  var requestId = 0;
45
72
  var acceptCallbacks = {};
73
+ var refreshTimer = null;
46
74
  log("Connecting to", url);
47
75
  var ws = new WebSocket(url);
48
76
  function sendChanges(changes2, baseRevision2, partial2, onChangesAccepted2) {
@@ -59,30 +87,71 @@ var init_syncProtocol = __esm({
59
87
  })
60
88
  );
61
89
  }
62
- ws.onopen = function(event) {
63
- log("Opening socket - sending clientIdentity", context.clientIdentity);
64
- ws.send(
65
- JSON.stringify({
66
- type: "clientIdentity",
67
- clientIdentity: context.clientIdentity || null,
68
- authToken: options.authToken,
69
- schema: options.schema
70
- })
71
- );
90
+ function clearRefreshTimer() {
91
+ if (refreshTimer) {
92
+ clearTimeout(refreshTimer);
93
+ refreshTimer = null;
94
+ }
95
+ }
96
+ function resolveGetToken() {
97
+ var fn = getTokenGetter(url);
98
+ if (!fn) throw new Error("No token getter registered for " + url);
99
+ return fn;
100
+ }
101
+ function scheduleTokenRefresh(tokenStr) {
102
+ clearRefreshTimer();
103
+ var exp = decodeJwtExp(tokenStr);
104
+ if (!exp) return;
105
+ var msUntilRefresh = (exp - TOKEN_REFRESH_BUFFER) * 1e3 - Date.now();
106
+ if (msUntilRefresh <= 0) return;
107
+ log("Scheduling proactive token refresh in", Math.round(msUntilRefresh / 1e3), "s");
108
+ refreshTimer = setTimeout(async function() {
109
+ try {
110
+ var newToken = await resolveGetToken()({ forceRefresh: true });
111
+ if (ws.readyState === WebSocket.OPEN) {
112
+ log("Sending tokenUpdate on existing WebSocket");
113
+ ws.send(JSON.stringify({ type: "tokenUpdate", authToken: newToken }));
114
+ scheduleTokenRefresh(newToken);
115
+ }
116
+ } catch (err) {
117
+ log("Proactive token refresh failed (non-fatal):", err);
118
+ }
119
+ }, msUntilRefresh);
120
+ }
121
+ ws.onopen = async function(event) {
122
+ try {
123
+ var token = await resolveGetToken()();
124
+ log("Opening socket - sending clientIdentity", context.clientIdentity);
125
+ ws.send(
126
+ JSON.stringify({
127
+ type: "clientIdentity",
128
+ clientIdentity: context.clientIdentity || null,
129
+ authToken: token,
130
+ schema: options.schema
131
+ })
132
+ );
133
+ scheduleTokenRefresh(token);
134
+ } catch (err) {
135
+ log("Failed to get token for WebSocket:", err);
136
+ ws.close();
137
+ onError("Authentication failed: " + (err.message || err), RECONNECT_DELAY);
138
+ }
72
139
  };
73
140
  ws.onerror = function(event) {
141
+ clearRefreshTimer();
74
142
  ws.close();
75
143
  log("ws.onerror", event);
76
144
  onError(event?.message, RECONNECT_DELAY);
77
145
  };
78
146
  ws.onclose = function(event) {
147
+ clearRefreshTimer();
79
148
  onError("Socket closed: " + event.reason, RECONNECT_DELAY);
80
149
  };
81
150
  var isFirstRound = true;
82
151
  ws.onmessage = function(event) {
83
152
  try {
84
153
  var requestFromServer = JSON.parse(event.data);
85
- log("requestFromServer", requestFromServer, { acceptCallback, isFirstRound });
154
+ log("requestFromServer", requestFromServer, { isFirstRound });
86
155
  if (requestFromServer.type == "clientIdentity") {
87
156
  context.clientIdentity = requestFromServer.clientIdentity;
88
157
  context.save();
@@ -110,8 +179,8 @@ var init_syncProtocol = __esm({
110
179
  onChangesAccepted2
111
180
  );
112
181
  },
113
- // Specify a disconnect function that will close our socket so that we dont continue to monitor changes.
114
182
  disconnect: function() {
183
+ clearRefreshTimer();
115
184
  ws.close();
116
185
  }
117
186
  });
@@ -123,9 +192,13 @@ var init_syncProtocol = __esm({
123
192
  acceptCallback();
124
193
  delete acceptCallbacks[requestId2.toString()];
125
194
  } else if (requestFromServer.type == "error") {
126
- var requestId2 = requestFromServer.requestId;
127
195
  ws.close();
128
- onError(requestFromServer.message, Infinity);
196
+ if (requestFromServer.code === "TOKEN_EXPIRED" || requestFromServer.code === "UNAUTHORIZED") {
197
+ log("Auth error from server, will reconnect with fresh token:", requestFromServer.message);
198
+ onError(requestFromServer.message, RECONNECT_DELAY);
199
+ } else {
200
+ onError(requestFromServer.message, Infinity);
201
+ }
129
202
  } else {
130
203
  log("unknown message", requestFromServer);
131
204
  ws.close();
@@ -145,22 +218,19 @@ var init_syncProtocol = __esm({
145
218
 
146
219
  // src/AuthContext.tsx
147
220
  import { createContext, useContext, useEffect, useState, useRef } from "react";
148
- import { jwtDecode } from "jwt-decode";
149
221
 
150
222
  // src/sync/index.ts
151
223
  init_config();
224
+ init_tokenRegistry();
152
225
  import { v7 as uuidv7 } from "uuid";
153
226
  import { Dexie as Dexie2 } from "dexie";
154
227
  import { validateData } from "@basictech/schema";
155
228
  var dexieExtensionsLoaded = false;
156
229
  var initPromise = null;
157
230
  async function initDexieExtensions() {
158
- if (dexieExtensionsLoaded)
159
- return;
160
- if (typeof window === "undefined")
161
- return;
162
- if (initPromise)
163
- return initPromise;
231
+ if (dexieExtensionsLoaded) return;
232
+ if (typeof window === "undefined") return;
233
+ if (initPromise) return initPromise;
164
234
  initPromise = (async () => {
165
235
  try {
166
236
  await import("dexie-syncable");
@@ -185,12 +255,13 @@ var BasicSync = class extends Dexie2 {
185
255
  this.version(2).stores({});
186
256
  this.Collection.prototype.get = this.Collection.prototype.toArray;
187
257
  }
188
- async connect({ access_token, ws_url }) {
258
+ async connect({ getToken, ws_url }) {
189
259
  const WS_URL = ws_url || "wss://pds.basic.id/ws";
190
260
  log("Connecting to", WS_URL);
261
+ setTokenGetter(WS_URL, getToken);
191
262
  await this.updateSyncNodes();
192
263
  log("Starting connection...");
193
- return this.syncable.connect("websocket", WS_URL, { authToken: access_token, schema: this.basic_schema });
264
+ return this.syncable.connect("websocket", WS_URL, { schema: this.basic_schema });
194
265
  }
195
266
  async disconnect({ ws_url } = {}) {
196
267
  const WS_URL = ws_url || "wss://pds.basic.id/ws";
@@ -230,7 +301,7 @@ var BasicSync = class extends Dexie2 {
230
301
  }
231
302
  _convertSchemaToDxSchema(schema) {
232
303
  const stores = Object.entries(schema.tables).map(([key, table]) => {
233
- const indexedFields = Object.entries(table.fields).filter(([key2, field]) => field.indexed).map(([key2, field]) => `,${key2}`).join("");
304
+ const indexedFields = Object.entries(table.fields).filter(([, field]) => field.indexed).map(([fieldKey]) => `,${fieldKey}`).join("");
234
305
  return {
235
306
  [key]: "id" + indexedFields
236
307
  };
@@ -397,29 +468,45 @@ var RemoteCollection = class {
397
468
  const token = await this.config.getToken();
398
469
  const url = `${this.config.serverUrl}${path}`;
399
470
  this.log(`${method} ${url}`, body ? JSON.stringify(body) : "");
471
+ const headers = {
472
+ "Authorization": `Bearer ${token}`
473
+ };
474
+ if (body) {
475
+ headers["Content-Type"] = "application/json";
476
+ }
400
477
  const response = await fetch(url, {
401
478
  method,
402
- headers: {
403
- "Content-Type": "application/json",
404
- "Authorization": `Bearer ${token}`
405
- },
479
+ headers,
406
480
  ...body ? { body: JSON.stringify(body) } : {}
407
481
  });
408
482
  const responseData = await response.json().catch(() => ({}));
409
483
  if (!response.ok) {
410
484
  if (response.status === 401 && !isRetry) {
411
- this.log("Got 401, retrying with fresh token...");
485
+ this.log("Got 401, forcing token refresh and retrying...");
486
+ await this.config.getToken({ forceRefresh: true });
412
487
  return this.request(method, path, body, true);
413
488
  }
414
489
  if (this.config.debug) {
415
490
  console.error(`[RemoteDB] Error ${response.status}:`, responseData);
416
491
  }
417
- if (response.status === 401 && this.config.onAuthError) {
418
- this.config.onAuthError({
419
- status: response.status,
420
- message: "Authentication failed",
421
- response: responseData
422
- });
492
+ if (this.config.onAuthError) {
493
+ if (response.status === 401) {
494
+ this.config.onAuthError({
495
+ status: response.status,
496
+ message: "Authentication failed",
497
+ response: responseData,
498
+ errorType: "expired",
499
+ afterRetry: isRetry
500
+ });
501
+ } else if (response.status === 403) {
502
+ this.config.onAuthError({
503
+ status: response.status,
504
+ message: responseData.message || "Forbidden - insufficient permissions or missing scope",
505
+ response: responseData,
506
+ errorType: "forbidden",
507
+ afterRetry: isRetry
508
+ });
509
+ }
423
510
  }
424
511
  const errorMessage = responseData.message || responseData.error || responseData.detail || (typeof responseData === "string" ? responseData : `API request failed: ${response.status}`);
425
512
  throw new RemoteDBError(errorMessage, response.status, responseData);
@@ -618,136 +705,8 @@ var RemoteDB = class {
618
705
  }
619
706
  };
620
707
 
621
- // src/AuthContext.tsx
622
- init_config();
623
-
624
- // package.json
625
- var version = "0.7.0-beta.6";
626
-
627
- // src/updater/versionUpdater.ts
628
- var VersionUpdater = class {
629
- storage;
630
- currentVersion;
631
- migrations;
632
- versionKey = "basic_app_version";
633
- constructor(storage, currentVersion, migrations = []) {
634
- this.storage = storage;
635
- this.currentVersion = currentVersion;
636
- this.migrations = migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
637
- }
638
- /**
639
- * Check current stored version and run migrations if needed
640
- * Only compares major.minor versions, ignoring beta/prerelease parts
641
- * Example: "0.7.0-beta.1" and "0.7.0" are treated as the same version
642
- */
643
- async checkAndUpdate() {
644
- const storedVersion = await this.getStoredVersion();
645
- if (!storedVersion) {
646
- await this.setStoredVersion(this.currentVersion);
647
- return { updated: false, toVersion: this.currentVersion };
648
- }
649
- if (storedVersion === this.currentVersion) {
650
- return { updated: false, toVersion: this.currentVersion };
651
- }
652
- const migrationsToRun = this.getMigrationsToRun(storedVersion, this.currentVersion);
653
- if (migrationsToRun.length === 0) {
654
- await this.setStoredVersion(this.currentVersion);
655
- return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion };
656
- }
657
- for (const migration of migrationsToRun) {
658
- try {
659
- console.log(`Running migration from ${migration.fromVersion} to ${migration.toVersion}`);
660
- await migration.migrate(this.storage);
661
- } catch (error) {
662
- console.error(`Migration failed from ${migration.fromVersion} to ${migration.toVersion}:`, error);
663
- throw new Error(`Migration failed: ${error}`);
664
- }
665
- }
666
- await this.setStoredVersion(this.currentVersion);
667
- return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion };
668
- }
669
- async getStoredVersion() {
670
- try {
671
- const versionData = await this.storage.get(this.versionKey);
672
- if (!versionData)
673
- return null;
674
- const versionInfo = JSON.parse(versionData);
675
- return versionInfo.version;
676
- } catch (error) {
677
- console.warn("Failed to get stored version:", error);
678
- return null;
679
- }
680
- }
681
- async setStoredVersion(version2) {
682
- const versionInfo = {
683
- version: version2,
684
- lastUpdated: Date.now()
685
- };
686
- await this.storage.set(this.versionKey, JSON.stringify(versionInfo));
687
- }
688
- getMigrationsToRun(fromVersion, toVersion) {
689
- return this.migrations.filter((migration) => {
690
- const storedLessThanMigrationTo = this.compareVersions(fromVersion, migration.toVersion) < 0;
691
- const currentGreaterThanOrEqualMigrationTo = this.compareVersions(toVersion, migration.toVersion) >= 0;
692
- console.log(`Checking migration ${migration.fromVersion} \u2192 ${migration.toVersion}:`);
693
- console.log(` stored ${fromVersion} < migration.to ${migration.toVersion}: ${storedLessThanMigrationTo}`);
694
- console.log(` current ${toVersion} >= migration.to ${migration.toVersion}: ${currentGreaterThanOrEqualMigrationTo}`);
695
- const shouldRun = storedLessThanMigrationTo && currentGreaterThanOrEqualMigrationTo;
696
- console.log(` Should run: ${shouldRun}`);
697
- return shouldRun;
698
- });
699
- }
700
- /**
701
- * Simple semantic version comparison (major.minor only, ignoring beta/prerelease)
702
- * Returns: -1 if a < b, 0 if a === b, 1 if a > b
703
- */
704
- compareVersions(a, b) {
705
- const aMajorMinor = this.extractMajorMinor(a);
706
- const bMajorMinor = this.extractMajorMinor(b);
707
- if (aMajorMinor.major !== bMajorMinor.major) {
708
- return aMajorMinor.major - bMajorMinor.major;
709
- }
710
- return aMajorMinor.minor - bMajorMinor.minor;
711
- }
712
- /**
713
- * Extract major.minor from version string, ignoring beta/prerelease
714
- * Examples: "0.7.0-beta.1" -> {major: 0, minor: 7}
715
- * "1.2.3" -> {major: 1, minor: 2}
716
- */
717
- extractMajorMinor(version2) {
718
- const cleanVersion = version2.split("-")[0]?.split("+")[0] || version2;
719
- const parts = cleanVersion.split(".").map(Number);
720
- return {
721
- major: parts[0] || 0,
722
- minor: parts[1] || 0
723
- };
724
- }
725
- /**
726
- * Add a migration to the updater
727
- */
728
- addMigration(migration) {
729
- this.migrations.push(migration);
730
- this.migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
731
- }
732
- };
733
- function createVersionUpdater(storage, currentVersion, migrations = []) {
734
- return new VersionUpdater(storage, currentVersion, migrations);
735
- }
736
-
737
- // src/updater/updateMigrations.ts
738
- var addMigrationTimestamp = {
739
- fromVersion: "0.6.0",
740
- toVersion: "0.7.0",
741
- async migrate(storage) {
742
- console.log("Running test migration");
743
- storage.set("test_migration", "true");
744
- }
745
- };
746
- function getMigrations() {
747
- return [
748
- addMigrationTimestamp
749
- ];
750
- }
708
+ // src/core/auth/AuthManager.ts
709
+ import { jwtDecode } from "jwt-decode";
751
710
 
752
711
  // src/utils/storage.ts
753
712
  var LocalStorageAdapter = class {
@@ -767,44 +726,95 @@ var STORAGE_KEYS = {
767
726
  AUTH_STATE: "basic_auth_state",
768
727
  REDIRECT_URI: "basic_redirect_uri",
769
728
  SERVER_URL: "basic_server_url",
770
- DEBUG: "basic_debug"
729
+ PDS_ENDPOINTS: "basic_pds_endpoints",
730
+ LAST_CONNECT_REPORT: "basic_last_connect_report",
731
+ DEBUG: "basic_debug",
732
+ CODE_VERIFIER: "basic_code_verifier"
771
733
  };
772
- function getCookie(name) {
773
- let cookieValue = "";
774
- if (document.cookie && document.cookie !== "") {
775
- const cookies = document.cookie.split(";");
776
- for (let i = 0; i < cookies.length; i++) {
777
- const cookie = cookies[i]?.trim();
778
- if (cookie && cookie.substring(0, name.length + 1) === name + "=") {
779
- cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
780
- break;
781
- }
782
- }
734
+
735
+ // src/utils/normalizeClientId.ts
736
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
737
+ function normalizeClientId(projectId, adminHostname = "api.basic.tech") {
738
+ if (!projectId) return projectId;
739
+ if (projectId === "self") return projectId;
740
+ if (projectId.startsWith("did:")) return projectId;
741
+ if (UUID_RE.test(projectId)) {
742
+ const hex = projectId.replace(/-/g, "").toLowerCase();
743
+ return `did:web:${adminHostname}:projects:${hex}`;
744
+ }
745
+ return projectId;
746
+ }
747
+
748
+ // src/utils/resolveDid.ts
749
+ function resolveDidWebUrl(did) {
750
+ if (!did.startsWith("did:web:")) return null;
751
+ const rest = did.slice(8);
752
+ if (!rest) return null;
753
+ const parts = rest.split(":");
754
+ const hostname = parts[0].replace(/%3A/gi, ":");
755
+ if (parts.length === 1) {
756
+ return `https://${hostname}/.well-known/did.json`;
783
757
  }
784
- return cookieValue;
758
+ const pathParts = parts.slice(1).map((p) => decodeURIComponent(p));
759
+ return `https://${hostname}/${pathParts.join("/")}/did.json`;
785
760
  }
786
- function setCookie(name, value, options) {
787
- const opts = {
788
- secure: true,
789
- sameSite: "Strict",
790
- httpOnly: false,
791
- ...options
761
+ async function resolveFromDocument(did, didDocument) {
762
+ const services = didDocument.service;
763
+ const pdsService = services?.find(
764
+ (s) => s.id === "#basic_pds" || s.id === `${did}#basic_pds`
765
+ );
766
+ if (!pdsService) {
767
+ throw new Error(`DID document has no #basic_pds service entry`);
768
+ }
769
+ const pdsUrl = pdsService.serviceEndpoint.replace(/\/+$/, "");
770
+ const oauthRes = await fetch(`${pdsUrl}/auth/.well-known/openid-configuration`);
771
+ if (!oauthRes.ok) {
772
+ throw new Error(`Failed to fetch OpenID configuration from ${pdsUrl}: ${oauthRes.status}`);
773
+ }
774
+ const oauth = await oauthRes.json();
775
+ return {
776
+ did,
777
+ didDocument,
778
+ pdsUrl,
779
+ authorization_endpoint: oauth.authorization_endpoint,
780
+ token_endpoint: oauth.token_endpoint,
781
+ userinfo_endpoint: oauth.userinfo_endpoint
792
782
  };
793
- let cookieString = `${name}=${value}`;
794
- if (opts.secure)
795
- cookieString += "; Secure";
796
- if (opts.sameSite)
797
- cookieString += `; SameSite=${opts.sameSite}`;
798
- if (opts.httpOnly)
799
- cookieString += "; HttpOnly";
800
- document.cookie = cookieString;
801
783
  }
802
- function clearCookie(name) {
803
- document.cookie = `${name}=; Secure; SameSite=Strict`;
784
+ async function resolveDid(did) {
785
+ const url = resolveDidWebUrl(did);
786
+ if (!url) {
787
+ throw new Error(`Unsupported DID method: ${did}`);
788
+ }
789
+ const didRes = await fetch(url);
790
+ if (!didRes.ok) {
791
+ throw new Error(`Failed to fetch DID document at ${url}: ${didRes.status}`);
792
+ }
793
+ const didDocument = await didRes.json();
794
+ return resolveFromDocument(did, didDocument);
795
+ }
796
+ async function resolveHandle(handle) {
797
+ const res = await fetch(`https://${handle}/.well-known/did.json`);
798
+ if (!res.ok) {
799
+ throw new Error(`Handle resolution failed for ${handle}: ${res.status}`);
800
+ }
801
+ const didDocument = await res.json();
802
+ const did = didDocument.id;
803
+ if (!did) {
804
+ throw new Error(`Handle response has no 'id' field`);
805
+ }
806
+ const resolved = await resolveFromDocument(did, didDocument);
807
+ resolved.handle = handle;
808
+ return resolved;
804
809
  }
805
810
 
806
811
  // src/utils/network.ts
807
812
  init_config();
813
+
814
+ // package.json
815
+ var version = "0.8.0-beta.1";
816
+
817
+ // src/utils/network.ts
808
818
  function isDevelopment(debug) {
809
819
  return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname.includes("localhost") || window.location.hostname.includes("127.0.0.1") || window.location.hostname.includes(".local") || process.env.NODE_ENV === "development" || debug === true;
810
820
  }
@@ -866,85 +876,909 @@ function getSyncStatus(statusCode) {
866
876
  }
867
877
  }
868
878
 
869
- // src/utils/schema.ts
879
+ // src/core/auth/AuthManager.ts
870
880
  init_config();
871
- import { validateSchema as validateSchema2, compareSchemas } from "@basictech/schema";
872
- async function getSchemaStatus(schema) {
873
- const projectId = schema.project_id;
874
- const valid = validateSchema2(schema);
875
- if (!valid.valid) {
876
- console.warn("BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled");
877
- return {
878
- valid: false,
879
- status: "invalid",
880
- latest: null
881
- };
881
+ function generateCodeVerifier() {
882
+ const array = new Uint8Array(32);
883
+ crypto.getRandomValues(array);
884
+ return base64UrlEncode(array);
885
+ }
886
+ async function generateCodeChallenge(verifier) {
887
+ if (typeof crypto === "undefined" || !crypto.subtle) {
888
+ log("crypto.subtle unavailable (non-secure context?) -- falling back to plain PKCE challenge");
889
+ return { challenge: verifier, method: "plain" };
882
890
  }
883
- const latestSchema = await fetch(`https://api.basic.tech/project/${projectId}/schema`).then((res) => res.json()).then((data) => data.data[0].schema).catch((err) => {
884
- return {
885
- valid: false,
886
- status: "error",
887
- latest: null
888
- };
889
- });
890
- console.log("latestSchema", latestSchema);
891
- if (!latestSchema.version) {
892
- return {
893
- valid: false,
894
- status: "error",
895
- latest: null
896
- };
891
+ const encoder = new TextEncoder();
892
+ const data = encoder.encode(verifier);
893
+ const digest = await crypto.subtle.digest("SHA-256", data);
894
+ return { challenge: base64UrlEncode(new Uint8Array(digest)), method: "S256" };
895
+ }
896
+ function base64UrlEncode(buffer) {
897
+ let str = "";
898
+ for (let i = 0; i < buffer.length; i++) {
899
+ str += String.fromCharCode(buffer[i]);
897
900
  }
898
- if (latestSchema.version > schema.version) {
899
- console.warn("BasicDB Error: your local schema version is behind the latest. Found version:", schema.version, "but expected", latestSchema.version, " - sync is disabled");
900
- return {
901
- valid: false,
902
- status: "behind",
903
- latest: latestSchema
904
- };
905
- } else if (latestSchema.version < schema.version) {
906
- console.warn("BasicDB Error: your local schema version is ahead of the latest. Found version:", schema.version, "but expected", latestSchema.version, " - sync is disabled");
907
- return {
908
- valid: false,
909
- status: "ahead",
910
- latest: latestSchema
911
- };
912
- } else if (latestSchema.version === schema.version) {
913
- const changes = compareSchemas(schema, latestSchema);
914
- if (changes.valid) {
915
- return {
916
- valid: true,
917
- status: "current",
918
- latest: latestSchema
919
- };
920
- } else {
921
- console.warn("BasicDB Error: your local schema is conflicting with the latest. Your version:", schema.version, "does not match origin version", latestSchema.version, " - sync is disabled");
922
- return {
923
- valid: false,
924
- status: "conflict",
925
- latest: latestSchema
901
+ return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
902
+ }
903
+ var AuthManager = class {
904
+ // --- Public state (read by the UI layer) ---
905
+ token = null;
906
+ user = null;
907
+ isSignedIn = false;
908
+ isAuthReady = false;
909
+ did = null;
910
+ /** Space-separated scopes granted in the current access token */
911
+ tokenScope = null;
912
+ /** Space-separated scopes originally requested in the auth config */
913
+ requestedScopes;
914
+ config;
915
+ storage;
916
+ /** True only during a user-initiated OAuth code exchange (not session restore) */
917
+ freshSignIn = false;
918
+ // --- Private ---
919
+ notify;
920
+ refreshPromise = null;
921
+ codeExchangePromise = null;
922
+ pendingRefresh = false;
923
+ isOnline = typeof navigator !== "undefined" ? navigator.onLine : true;
924
+ channel = null;
925
+ constructor(config, storage, notify) {
926
+ this.config = config;
927
+ this.storage = storage;
928
+ this.notify = notify;
929
+ this.requestedScopes = config.scopes;
930
+ this.initCrossTabSync();
931
+ }
932
+ initCrossTabSync() {
933
+ if (typeof BroadcastChannel === "undefined") return;
934
+ try {
935
+ this.channel = new BroadcastChannel("basic-auth");
936
+ this.channel.onmessage = (event) => {
937
+ if (event.data?.type === "token_refreshed") {
938
+ log("Received token refresh from another tab");
939
+ if (event.data.accessToken && this.token) {
940
+ this.token = { ...this.token, access_token: event.data.accessToken };
941
+ }
942
+ if (event.data.did) this.did = event.data.did;
943
+ if (event.data.tokenScope) this.tokenScope = event.data.tokenScope;
944
+ this.notify();
945
+ }
946
+ if (event.data?.type === "signed_in") {
947
+ log("Received sign-in from another tab, reloading");
948
+ if (typeof window !== "undefined") {
949
+ window.location.reload();
950
+ }
951
+ }
952
+ if (event.data?.type === "signed_out") {
953
+ log("Received sign-out from another tab, reloading");
954
+ this.user = null;
955
+ this.isSignedIn = false;
956
+ this.token = null;
957
+ this.did = null;
958
+ this.tokenScope = null;
959
+ this.notify();
960
+ if (typeof window !== "undefined") {
961
+ window.location.reload();
962
+ }
963
+ }
926
964
  };
965
+ } catch {
966
+ log("BroadcastChannel not available for cross-tab sync");
927
967
  }
928
- } else {
929
- return {
930
- valid: false,
931
- status: "error",
932
- latest: null
933
- };
934
968
  }
935
- }
936
- async function validateAndCheckSchema(schema) {
937
- const valid = validateSchema2(schema);
938
- if (!valid.valid) {
939
- log("Basic Schema is invalid!", valid.errors);
940
- console.group("Schema Errors");
941
- let errorMessage = "";
942
- valid.errors.forEach((error, index) => {
943
- log(`${index + 1}:`, error.message, ` - at ${error.instancePath}`);
944
- errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}
945
- `;
969
+ broadcastTokenRefresh() {
970
+ this.channel?.postMessage({
971
+ type: "token_refreshed",
972
+ accessToken: this.token?.access_token,
973
+ did: this.did,
974
+ tokenScope: this.tokenScope
946
975
  });
947
- console.groupEnd();
976
+ }
977
+ broadcastSignIn() {
978
+ this.channel?.postMessage({ type: "signed_in" });
979
+ }
980
+ broadcastSignOut() {
981
+ this.channel?.postMessage({ type: "signed_out" });
982
+ }
983
+ // ------------------------------------------------------------------
984
+ // Public API
985
+ // ------------------------------------------------------------------
986
+ /**
987
+ * Bootstrap auth: handle OAuth callback (?code=), restore session
988
+ * from refresh token, or load cached user for offline mode.
989
+ */
990
+ async initialize() {
991
+ await this.storage.set(STORAGE_KEYS.DEBUG, this.config.debug ? "true" : "false");
992
+ const storedServerUrl = await this.storage.get(STORAGE_KEYS.SERVER_URL);
993
+ if (storedServerUrl && storedServerUrl !== this.config.pdsUrl) {
994
+ log("PDS URL changed, clearing stored tokens");
995
+ await this.clearStoredAuth();
996
+ }
997
+ await this.storage.set(STORAGE_KEYS.SERVER_URL, this.config.pdsUrl);
998
+ try {
999
+ const params = new URLSearchParams(window.location.search);
1000
+ if (params.has("code")) {
1001
+ const code = params.get("code");
1002
+ if (!code) {
1003
+ this.isAuthReady = true;
1004
+ this.notify();
1005
+ return;
1006
+ }
1007
+ const state = await this.storage.get(STORAGE_KEYS.AUTH_STATE);
1008
+ const urlState = params.get("state");
1009
+ if (!state || state !== urlState) {
1010
+ log("error: auth state does not match");
1011
+ this.isAuthReady = true;
1012
+ this.notify();
1013
+ await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
1014
+ cleanOAuthParamsFromUrl();
1015
+ return;
1016
+ }
1017
+ await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
1018
+ cleanOAuthParamsFromUrl();
1019
+ this.freshSignIn = true;
1020
+ this.exchangeToken(code, false).catch((error) => {
1021
+ log("Error fetching token:", error);
1022
+ });
1023
+ } else {
1024
+ const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1025
+ if (refreshToken) {
1026
+ log("Found refresh token in storage, attempting to refresh access token");
1027
+ this.exchangeToken(refreshToken, true).catch((error) => {
1028
+ log("Error fetching refresh token:", error);
1029
+ });
1030
+ } else {
1031
+ const cachedUserInfo = await this.storage.get(STORAGE_KEYS.USER_INFO);
1032
+ if (cachedUserInfo) {
1033
+ try {
1034
+ this.user = JSON.parse(cachedUserInfo);
1035
+ this.isSignedIn = true;
1036
+ log("Loaded cached user info for offline mode");
1037
+ } catch (error) {
1038
+ log("Error parsing cached user info:", error);
1039
+ }
1040
+ }
1041
+ this.isAuthReady = true;
1042
+ this.notify();
1043
+ }
1044
+ }
1045
+ } catch (e) {
1046
+ log("error getting token", e);
1047
+ }
1048
+ }
1049
+ /**
1050
+ * Get a valid access token string. Refreshes proactively (5s buffer)
1051
+ * or on demand (forceRefresh). Mutex prevents concurrent refreshes.
1052
+ */
1053
+ async getToken(options) {
1054
+ log("getting token...");
1055
+ if (!this.token) {
1056
+ const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1057
+ if (refreshToken) {
1058
+ log("No token in memory, attempting to refresh from storage");
1059
+ if (this.refreshPromise) {
1060
+ log("Token refresh already in progress, waiting...");
1061
+ try {
1062
+ const newToken = await this.refreshPromise;
1063
+ if (newToken?.access_token) {
1064
+ return newToken.access_token;
1065
+ }
1066
+ } catch (error) {
1067
+ log("In-flight refresh failed:", error);
1068
+ throw error;
1069
+ }
1070
+ }
1071
+ try {
1072
+ const newToken = await this.exchangeToken(refreshToken, true);
1073
+ if (newToken?.access_token) {
1074
+ return newToken.access_token;
1075
+ }
1076
+ } catch (error) {
1077
+ log("Failed to refresh token from storage:", error);
1078
+ if (this.isNetworkError(error)) {
1079
+ throw new Error("Network offline - authentication will be retried when online");
1080
+ }
1081
+ throw new Error("Authentication expired. Please sign in again.");
1082
+ }
1083
+ }
1084
+ log("no token found");
1085
+ throw new Error("no token found");
1086
+ }
1087
+ const decoded = jwtDecode(this.token.access_token);
1088
+ const expirationBuffer = 5;
1089
+ const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
1090
+ const shouldRefresh = isExpired || options?.forceRefresh === true;
1091
+ if (shouldRefresh) {
1092
+ log(options?.forceRefresh ? "force refreshing token..." : "token is expired - refreshing ...");
1093
+ if (this.refreshPromise) {
1094
+ log("Token refresh already in progress, waiting...");
1095
+ try {
1096
+ const newToken = await this.refreshPromise;
1097
+ return newToken?.access_token || "";
1098
+ } catch (error) {
1099
+ log("In-flight refresh failed:", error);
1100
+ if (this.isNetworkError(error)) {
1101
+ log("Network issue - using expired token until network is restored");
1102
+ return this.token.access_token;
1103
+ }
1104
+ throw error;
1105
+ }
1106
+ }
1107
+ const refreshToken = this.token.refresh_token || await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1108
+ if (refreshToken) {
1109
+ try {
1110
+ const newToken = await this.exchangeToken(refreshToken, true);
1111
+ return newToken?.access_token || "";
1112
+ } catch (error) {
1113
+ log("Failed to refresh expired token:", error);
1114
+ if (this.isNetworkError(error)) {
1115
+ log("Network issue - using expired token until network is restored");
1116
+ return this.token.access_token;
1117
+ }
1118
+ throw new Error("Authentication expired. Please sign in again.");
1119
+ }
1120
+ } else {
1121
+ throw new Error("no refresh token available");
1122
+ }
1123
+ }
1124
+ return this.token.access_token || "";
1125
+ }
1126
+ async getSignInUrl(redirectUri, endpoints) {
1127
+ log("getting sign in link...");
1128
+ if (!this.config.projectId) {
1129
+ throw new Error("Project ID is required to generate sign-in link");
1130
+ }
1131
+ const pdsEndpoints = endpoints || this.defaultPdsEndpoints();
1132
+ await this.storage.set(STORAGE_KEYS.PDS_ENDPOINTS, JSON.stringify(pdsEndpoints));
1133
+ const randomState = Math.random().toString(36).substring(6);
1134
+ await this.storage.set(STORAGE_KEYS.AUTH_STATE, randomState);
1135
+ const redirectUrl = redirectUri || window.location.href;
1136
+ if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
1137
+ throw new Error("Invalid redirect URI provided");
1138
+ }
1139
+ await this.storage.set(STORAGE_KEYS.REDIRECT_URI, redirectUrl);
1140
+ log("Stored redirect_uri for token exchange:", redirectUrl);
1141
+ const codeVerifier = generateCodeVerifier();
1142
+ const { challenge: codeChallenge, method: challengeMethod } = await generateCodeChallenge(codeVerifier);
1143
+ await this.storage.set(STORAGE_KEYS.CODE_VERIFIER, codeVerifier);
1144
+ let baseUrl = pdsEndpoints.authorization_endpoint;
1145
+ baseUrl += `?client_id=${encodeURIComponent(normalizeClientId(this.config.projectId, this.adminHostname))}`;
1146
+ baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`;
1147
+ baseUrl += `&response_type=code`;
1148
+ baseUrl += `&scope=${encodeURIComponent(this.config.scopes)}`;
1149
+ baseUrl += `&state=${randomState}`;
1150
+ baseUrl += `&code_challenge=${encodeURIComponent(codeChallenge)}`;
1151
+ baseUrl += `&code_challenge_method=${challengeMethod}`;
1152
+ log("Generated sign-in link successfully with scopes:", this.config.scopes);
1153
+ return baseUrl;
1154
+ }
1155
+ async signIn(redirectUri) {
1156
+ log("signing in...");
1157
+ if (!this.config.projectId) {
1158
+ log("Error: project_id is required for sign-in");
1159
+ throw new Error("Project ID is required for authentication");
1160
+ }
1161
+ const signInLink = await this.getSignInUrl(redirectUri);
1162
+ log("Generated sign-in link:", signInLink);
1163
+ try {
1164
+ new URL(signInLink);
1165
+ } catch {
1166
+ log("Error: Invalid sign-in link generated");
1167
+ throw new Error("Failed to generate valid sign-in URL");
1168
+ }
1169
+ window.location.href = signInLink;
1170
+ }
1171
+ async signInWithHandle(handle) {
1172
+ log("signing in with handle:", handle);
1173
+ if (!this.config.projectId) {
1174
+ throw new Error("Project ID is required for authentication");
1175
+ }
1176
+ const resolved = await resolveHandle(handle);
1177
+ log("Resolved handle to PDS:", resolved.pdsUrl);
1178
+ const endpoints = {
1179
+ pds_url: resolved.pdsUrl,
1180
+ authorization_endpoint: resolved.authorization_endpoint,
1181
+ token_endpoint: resolved.token_endpoint,
1182
+ userinfo_endpoint: resolved.userinfo_endpoint
1183
+ };
1184
+ const signInLink = await this.getSignInUrl(void 0, endpoints);
1185
+ log("Generated federated sign-in link:", signInLink);
1186
+ try {
1187
+ new URL(signInLink);
1188
+ } catch {
1189
+ throw new Error("Failed to generate valid sign-in URL");
1190
+ }
1191
+ window.location.href = signInLink;
1192
+ }
1193
+ async signInWithCode(code, state) {
1194
+ try {
1195
+ log("signInWithCode called with code:", code);
1196
+ if (!code || typeof code !== "string") {
1197
+ return { success: false, error: "Invalid authorization code" };
1198
+ }
1199
+ if (state) {
1200
+ const storedState = await this.storage.get(STORAGE_KEYS.AUTH_STATE);
1201
+ if (storedState && storedState !== state) {
1202
+ log("State parameter mismatch:", { provided: state, stored: storedState });
1203
+ return { success: false, error: "State parameter mismatch" };
1204
+ }
1205
+ }
1206
+ await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
1207
+ cleanOAuthParamsFromUrl();
1208
+ this.freshSignIn = true;
1209
+ const token = await this.exchangeToken(code, false);
1210
+ if (token) {
1211
+ log("signInWithCode successful");
1212
+ return { success: true };
1213
+ } else {
1214
+ return { success: false, error: "Failed to exchange code for token" };
1215
+ }
1216
+ } catch (error) {
1217
+ log("signInWithCode error:", error);
1218
+ return {
1219
+ success: false,
1220
+ error: error.message || "Authentication failed"
1221
+ };
1222
+ }
1223
+ }
1224
+ /**
1225
+ * Clear auth state and storage. Does NOT handle sync/DB cleanup —
1226
+ * the UI layer (BasicProvider) wraps this to add sync teardown.
1227
+ */
1228
+ async signOut() {
1229
+ log("signing out!");
1230
+ this.resetAuthState();
1231
+ await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
1232
+ await this.storage.remove(STORAGE_KEYS.LAST_CONNECT_REPORT);
1233
+ await this.clearStoredAuth();
1234
+ this.broadcastSignOut();
1235
+ this.notify();
1236
+ }
1237
+ hasScope(scope) {
1238
+ if (!this.tokenScope) return false;
1239
+ return this.tokenScope.split(/[\s,]+/).filter(Boolean).includes(scope);
1240
+ }
1241
+ /**
1242
+ * Returns scopes that were requested but not granted in the current token.
1243
+ * Useful after login or when a 403 is returned.
1244
+ */
1245
+ missingScopes() {
1246
+ const requested = this.requestedScopes.split(/[\s,]+/).filter(Boolean);
1247
+ if (!this.tokenScope) return requested;
1248
+ const granted = new Set(this.tokenScope.split(/[\s,]+/).filter(Boolean));
1249
+ return requested.filter((s) => !granted.has(s));
1250
+ }
1251
+ /**
1252
+ * Register online/offline handlers that retry pending refreshes.
1253
+ * Returns a cleanup function for useEffect teardown.
1254
+ */
1255
+ setupNetworkListeners() {
1256
+ const handleOnline = async () => {
1257
+ log("Network came back online");
1258
+ this.isOnline = true;
1259
+ if (this.pendingRefresh && this.token) {
1260
+ log("Retrying pending token refresh");
1261
+ this.pendingRefresh = false;
1262
+ const refreshToken = this.token.refresh_token || await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1263
+ if (refreshToken) {
1264
+ this.exchangeToken(refreshToken, true).catch((error) => {
1265
+ log("Retry refresh failed:", error);
1266
+ });
1267
+ }
1268
+ }
1269
+ };
1270
+ const handleOffline = () => {
1271
+ log("Network went offline");
1272
+ this.isOnline = false;
1273
+ };
1274
+ window.addEventListener("online", handleOnline);
1275
+ window.addEventListener("offline", handleOffline);
1276
+ return () => {
1277
+ window.removeEventListener("online", handleOnline);
1278
+ window.removeEventListener("offline", handleOffline);
1279
+ };
1280
+ }
1281
+ // ------------------------------------------------------------------
1282
+ // Private
1283
+ // ------------------------------------------------------------------
1284
+ get adminHostname() {
1285
+ try {
1286
+ return new URL(this.config.adminUrl).hostname;
1287
+ } catch {
1288
+ return "api.basic.tech";
1289
+ }
1290
+ }
1291
+ defaultPdsEndpoints() {
1292
+ return {
1293
+ pds_url: this.config.pdsUrl,
1294
+ authorization_endpoint: `${this.config.pdsUrl}/auth/authorize`,
1295
+ token_endpoint: `${this.config.pdsUrl}/auth/token`,
1296
+ userinfo_endpoint: `${this.config.pdsUrl}/auth/userinfo`
1297
+ };
1298
+ }
1299
+ async getActivePdsEndpoints() {
1300
+ const stored = await this.storage.get(STORAGE_KEYS.PDS_ENDPOINTS);
1301
+ if (stored) {
1302
+ try {
1303
+ return JSON.parse(stored);
1304
+ } catch {
1305
+ }
1306
+ }
1307
+ return this.defaultPdsEndpoints();
1308
+ }
1309
+ async reportConnection(accessToken) {
1310
+ if (!this.config.projectId || !this.config.adminUrl) return;
1311
+ const lastReport = await this.storage.get(STORAGE_KEYS.LAST_CONNECT_REPORT);
1312
+ if (lastReport) {
1313
+ const elapsed = Date.now() - parseInt(lastReport, 10);
1314
+ if (elapsed < 24 * 60 * 60 * 1e3) return;
1315
+ }
1316
+ try {
1317
+ await fetch(`${this.config.adminUrl}/project/${this.config.projectId}/user/connect`, {
1318
+ method: "POST",
1319
+ headers: { "Content-Type": "application/json" },
1320
+ body: JSON.stringify({ token: accessToken })
1321
+ });
1322
+ await this.storage.set(STORAGE_KEYS.LAST_CONNECT_REPORT, Date.now().toString());
1323
+ log("Reported connection to admin server");
1324
+ } catch (err) {
1325
+ log("Failed to report connection (non-blocking):", err);
1326
+ }
1327
+ }
1328
+ /**
1329
+ * After a new token is stored, decode JWT claims and fetch user info.
1330
+ */
1331
+ async processNewToken() {
1332
+ if (!this.token) {
1333
+ this.isAuthReady = true;
1334
+ this.notify();
1335
+ return;
1336
+ }
1337
+ try {
1338
+ const decoded = jwtDecode(this.token.access_token);
1339
+ if (decoded.sub) this.did = decoded.sub;
1340
+ if (decoded.scope) this.tokenScope = decoded.scope;
1341
+ const expirationBuffer = 5;
1342
+ const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
1343
+ if (isExpired) {
1344
+ log("token is expired - refreshing ...");
1345
+ const refreshToken = this.token.refresh_token;
1346
+ if (!refreshToken) {
1347
+ log("Error: No refresh token available for expired token");
1348
+ this.isAuthReady = true;
1349
+ this.notify();
1350
+ return;
1351
+ }
1352
+ try {
1353
+ const newToken = await this.exchangeToken(refreshToken, true);
1354
+ await this.fetchUser(newToken?.access_token || "");
1355
+ } catch (error) {
1356
+ log("Failed to refresh token in processNewToken:", error);
1357
+ if (this.isNetworkError(error)) {
1358
+ log("Network issue - continuing with expired token until online");
1359
+ await this.fetchUser(this.token.access_token);
1360
+ } else {
1361
+ this.isAuthReady = true;
1362
+ this.notify();
1363
+ }
1364
+ }
1365
+ } else {
1366
+ await this.fetchUser(this.token.access_token);
1367
+ }
1368
+ } catch (error) {
1369
+ log("Error processing token:", error);
1370
+ this.isAuthReady = true;
1371
+ this.notify();
1372
+ }
1373
+ }
1374
+ async fetchUser(accessToken) {
1375
+ log("fetching user");
1376
+ try {
1377
+ const endpoints = await this.getActivePdsEndpoints();
1378
+ const response = await fetch(endpoints.userinfo_endpoint, {
1379
+ method: "GET",
1380
+ headers: { "Authorization": `Bearer ${accessToken}` }
1381
+ });
1382
+ if (!response.ok) {
1383
+ throw new Error(`Failed to fetch user info: ${response.status}`);
1384
+ }
1385
+ const user = await response.json();
1386
+ if (user.error) {
1387
+ log("error fetching user", user.error);
1388
+ throw new Error(`User info error: ${user.error}`);
1389
+ }
1390
+ if (this.token?.refresh_token) {
1391
+ await this.storage.set(STORAGE_KEYS.REFRESH_TOKEN, this.token.refresh_token);
1392
+ }
1393
+ await this.storage.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user));
1394
+ log("Cached user info in storage");
1395
+ this.user = user;
1396
+ this.isSignedIn = true;
1397
+ this.isAuthReady = true;
1398
+ if (this.freshSignIn) {
1399
+ this.freshSignIn = false;
1400
+ this.broadcastSignIn();
1401
+ } else {
1402
+ this.broadcastTokenRefresh();
1403
+ }
1404
+ this.notify();
1405
+ } catch (error) {
1406
+ log("Failed to fetch user info:", error);
1407
+ this.isAuthReady = true;
1408
+ this.notify();
1409
+ }
1410
+ }
1411
+ /**
1412
+ * Exchange an auth code or refresh token for an access token.
1413
+ * Handles mutex (one in-flight refresh), token validation, and
1414
+ * triggers processNewToken on success.
1415
+ */
1416
+ async exchangeToken(codeOrRefreshToken, isRefreshToken) {
1417
+ if (!codeOrRefreshToken || codeOrRefreshToken.trim() === "") {
1418
+ const errorMsg = isRefreshToken ? "Refresh token is empty or undefined" : "Authorization code is empty or undefined";
1419
+ log("Error:", errorMsg);
1420
+ throw new Error(errorMsg);
1421
+ }
1422
+ if (isRefreshToken && this.refreshPromise) {
1423
+ log("Reusing in-flight refresh token request");
1424
+ return this.refreshPromise;
1425
+ }
1426
+ if (!isRefreshToken && this.codeExchangePromise) {
1427
+ log("Reusing in-flight code exchange request");
1428
+ return this.codeExchangePromise;
1429
+ }
1430
+ const tokenPromise = (async () => {
1431
+ try {
1432
+ if (!this.isOnline) {
1433
+ log("Network is offline, marking refresh as pending");
1434
+ this.pendingRefresh = true;
1435
+ throw new Error("Network offline - refresh will be retried when online");
1436
+ }
1437
+ const endpoints = await this.getActivePdsEndpoints();
1438
+ let requestBody;
1439
+ if (isRefreshToken) {
1440
+ requestBody = {
1441
+ grant_type: "refresh_token",
1442
+ refresh_token: codeOrRefreshToken
1443
+ };
1444
+ if (this.config.projectId) {
1445
+ requestBody.client_id = normalizeClientId(this.config.projectId, this.adminHostname);
1446
+ }
1447
+ } else {
1448
+ requestBody = {
1449
+ grant_type: "authorization_code",
1450
+ code: codeOrRefreshToken
1451
+ };
1452
+ const storedRedirectUri = await this.storage.get(STORAGE_KEYS.REDIRECT_URI);
1453
+ if (storedRedirectUri) {
1454
+ requestBody.redirect_uri = storedRedirectUri;
1455
+ log("Including redirect_uri in token exchange:", storedRedirectUri);
1456
+ } else {
1457
+ log("Warning: No redirect_uri found in storage for token exchange");
1458
+ }
1459
+ const codeVerifier = await this.storage.get(STORAGE_KEYS.CODE_VERIFIER);
1460
+ if (codeVerifier) {
1461
+ requestBody.code_verifier = codeVerifier;
1462
+ }
1463
+ if (this.config.projectId) {
1464
+ requestBody.client_id = normalizeClientId(this.config.projectId, this.adminHostname);
1465
+ }
1466
+ }
1467
+ log("Token exchange request body:", {
1468
+ ...requestBody,
1469
+ ...isRefreshToken ? { refresh_token: "[REDACTED]" } : { code: "[REDACTED]" },
1470
+ ...requestBody.code_verifier ? { code_verifier: "[REDACTED]" } : {}
1471
+ });
1472
+ const token = await fetch(endpoints.token_endpoint, {
1473
+ method: "POST",
1474
+ headers: { "Content-Type": "application/json" },
1475
+ body: JSON.stringify(requestBody)
1476
+ }).then((response) => response.json()).catch((error) => {
1477
+ log("Network error fetching token:", error);
1478
+ if (!this.isOnline) {
1479
+ this.pendingRefresh = true;
1480
+ throw new Error("Network offline - refresh will be retried when online");
1481
+ }
1482
+ throw new Error("Network error during token refresh");
1483
+ });
1484
+ if (token.access_token) {
1485
+ try {
1486
+ const decoded = jwtDecode(token.access_token);
1487
+ if (decoded.typ === "refresh") {
1488
+ log("Error: received refresh token as access token");
1489
+ throw new Error("Invalid token: received refresh token instead of access token");
1490
+ }
1491
+ } catch (decodeError) {
1492
+ if (decodeError.message.includes("Invalid token")) {
1493
+ throw decodeError;
1494
+ }
1495
+ log("Warning: could not decode access token for type check:", decodeError);
1496
+ }
1497
+ }
1498
+ if (token.error) {
1499
+ log("error fetching token", token.error);
1500
+ if (typeof token.error === "string" && (token.error.includes("network") || token.error.includes("timeout"))) {
1501
+ this.pendingRefresh = true;
1502
+ throw new Error("Network issue - refresh will be retried when online");
1503
+ }
1504
+ await this.clearStoredAuth();
1505
+ this.resetAuthState();
1506
+ this.notify();
1507
+ throw new Error(`Token refresh failed: ${token.error}`);
1508
+ } else {
1509
+ this.token = token;
1510
+ this.pendingRefresh = false;
1511
+ if (token.refresh_token) {
1512
+ await this.storage.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token);
1513
+ log("Updated refresh token in storage");
1514
+ }
1515
+ if (!isRefreshToken) {
1516
+ await this.storage.remove(STORAGE_KEYS.REDIRECT_URI);
1517
+ await this.storage.remove(STORAGE_KEYS.CODE_VERIFIER);
1518
+ log("Cleaned up redirect_uri and code_verifier from storage after successful exchange");
1519
+ }
1520
+ this.reportConnection(token.access_token).catch(() => {
1521
+ });
1522
+ await this.processNewToken();
1523
+ }
1524
+ return token;
1525
+ } catch (error) {
1526
+ log("Token refresh error:", error);
1527
+ if (!this.isNetworkError(error)) {
1528
+ await this.clearStoredAuth();
1529
+ this.resetAuthState();
1530
+ this.notify();
1531
+ }
1532
+ throw error;
1533
+ }
1534
+ })();
1535
+ if (isRefreshToken) {
1536
+ this.refreshPromise = tokenPromise;
1537
+ tokenPromise.finally(() => {
1538
+ if (this.refreshPromise === tokenPromise) {
1539
+ this.refreshPromise = null;
1540
+ log("Cleared refresh promise reference");
1541
+ }
1542
+ });
1543
+ } else {
1544
+ this.codeExchangePromise = tokenPromise;
1545
+ tokenPromise.finally(() => {
1546
+ if (this.codeExchangePromise === tokenPromise) {
1547
+ this.codeExchangePromise = null;
1548
+ log("Cleared code exchange promise reference");
1549
+ }
1550
+ });
1551
+ }
1552
+ return tokenPromise;
1553
+ }
1554
+ resetAuthState() {
1555
+ this.user = null;
1556
+ this.isSignedIn = false;
1557
+ this.token = null;
1558
+ this.did = null;
1559
+ this.tokenScope = null;
1560
+ this.isAuthReady = true;
1561
+ }
1562
+ async clearStoredAuth() {
1563
+ await this.storage.remove(STORAGE_KEYS.REFRESH_TOKEN);
1564
+ await this.storage.remove(STORAGE_KEYS.USER_INFO);
1565
+ await this.storage.remove(STORAGE_KEYS.REDIRECT_URI);
1566
+ await this.storage.remove(STORAGE_KEYS.CODE_VERIFIER);
1567
+ await this.storage.remove(STORAGE_KEYS.SERVER_URL);
1568
+ await this.storage.remove(STORAGE_KEYS.PDS_ENDPOINTS);
1569
+ }
1570
+ isNetworkError(error) {
1571
+ if (error instanceof Error) {
1572
+ return error.message.includes("offline") || error.message.includes("Network");
1573
+ }
1574
+ return false;
1575
+ }
1576
+ };
1577
+
1578
+ // src/AuthContext.tsx
1579
+ init_config();
1580
+
1581
+ // src/updater/versionUpdater.ts
1582
+ init_config();
1583
+ var VersionUpdater = class {
1584
+ storage;
1585
+ currentVersion;
1586
+ migrations;
1587
+ versionKey = "basic_app_version";
1588
+ constructor(storage, currentVersion, migrations = []) {
1589
+ this.storage = storage;
1590
+ this.currentVersion = currentVersion;
1591
+ this.migrations = migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
1592
+ }
1593
+ /**
1594
+ * Check current stored version and run migrations if needed
1595
+ * Only compares major.minor versions, ignoring beta/prerelease parts
1596
+ * Example: "0.7.0-beta.1" and "0.7.0" are treated as the same version
1597
+ */
1598
+ async checkAndUpdate() {
1599
+ const storedVersion = await this.getStoredVersion();
1600
+ if (!storedVersion) {
1601
+ await this.setStoredVersion(this.currentVersion);
1602
+ return { updated: false, toVersion: this.currentVersion };
1603
+ }
1604
+ if (storedVersion === this.currentVersion) {
1605
+ return { updated: false, toVersion: this.currentVersion };
1606
+ }
1607
+ const migrationsToRun = this.getMigrationsToRun(storedVersion, this.currentVersion);
1608
+ if (migrationsToRun.length === 0) {
1609
+ await this.setStoredVersion(this.currentVersion);
1610
+ return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion };
1611
+ }
1612
+ for (const migration of migrationsToRun) {
1613
+ try {
1614
+ log(`Running migration from ${migration.fromVersion} to ${migration.toVersion}`);
1615
+ await migration.migrate(this.storage);
1616
+ } catch (error) {
1617
+ console.error(`Migration failed from ${migration.fromVersion} to ${migration.toVersion}:`, error);
1618
+ throw new Error(`Migration failed: ${error}`);
1619
+ }
1620
+ }
1621
+ await this.setStoredVersion(this.currentVersion);
1622
+ return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion };
1623
+ }
1624
+ async getStoredVersion() {
1625
+ try {
1626
+ const versionData = await this.storage.get(this.versionKey);
1627
+ if (!versionData) return null;
1628
+ const versionInfo = JSON.parse(versionData);
1629
+ return versionInfo.version;
1630
+ } catch (error) {
1631
+ console.warn("Failed to get stored version:", error);
1632
+ return null;
1633
+ }
1634
+ }
1635
+ async setStoredVersion(version2) {
1636
+ const versionInfo = {
1637
+ version: version2,
1638
+ lastUpdated: Date.now()
1639
+ };
1640
+ await this.storage.set(this.versionKey, JSON.stringify(versionInfo));
1641
+ }
1642
+ getMigrationsToRun(fromVersion, toVersion) {
1643
+ return this.migrations.filter((migration) => {
1644
+ const storedLessThanMigrationTo = this.compareVersions(fromVersion, migration.toVersion) < 0;
1645
+ const currentGreaterThanOrEqualMigrationTo = this.compareVersions(toVersion, migration.toVersion) >= 0;
1646
+ const shouldRun = storedLessThanMigrationTo && currentGreaterThanOrEqualMigrationTo;
1647
+ log(`Migration ${migration.fromVersion} \u2192 ${migration.toVersion}: shouldRun=${shouldRun}`);
1648
+ return shouldRun;
1649
+ });
1650
+ }
1651
+ /**
1652
+ * Simple semantic version comparison (major.minor only, ignoring beta/prerelease)
1653
+ * Returns: -1 if a < b, 0 if a === b, 1 if a > b
1654
+ */
1655
+ compareVersions(a, b) {
1656
+ const aMajorMinor = this.extractMajorMinor(a);
1657
+ const bMajorMinor = this.extractMajorMinor(b);
1658
+ if (aMajorMinor.major !== bMajorMinor.major) {
1659
+ return aMajorMinor.major - bMajorMinor.major;
1660
+ }
1661
+ return aMajorMinor.minor - bMajorMinor.minor;
1662
+ }
1663
+ /**
1664
+ * Extract major.minor from version string, ignoring beta/prerelease
1665
+ * Examples: "0.7.0-beta.1" -> {major: 0, minor: 7}
1666
+ * "1.2.3" -> {major: 1, minor: 2}
1667
+ */
1668
+ extractMajorMinor(version2) {
1669
+ const cleanVersion = version2.split("-")[0]?.split("+")[0] || version2;
1670
+ const parts = cleanVersion.split(".").map(Number);
1671
+ return {
1672
+ major: parts[0] || 0,
1673
+ minor: parts[1] || 0
1674
+ };
1675
+ }
1676
+ /**
1677
+ * Add a migration to the updater
1678
+ */
1679
+ addMigration(migration) {
1680
+ this.migrations.push(migration);
1681
+ this.migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
1682
+ }
1683
+ };
1684
+ function createVersionUpdater(storage, currentVersion, migrations = []) {
1685
+ return new VersionUpdater(storage, currentVersion, migrations);
1686
+ }
1687
+
1688
+ // src/updater/updateMigrations.ts
1689
+ init_config();
1690
+ var addMigrationTimestamp = {
1691
+ fromVersion: "0.6.0",
1692
+ toVersion: "0.7.0",
1693
+ async migrate(storage) {
1694
+ log("Running migration 0.6.0 \u2192 0.7.0");
1695
+ storage.set("test_migration", "true");
1696
+ }
1697
+ };
1698
+ function getMigrations() {
1699
+ return [
1700
+ addMigrationTimestamp
1701
+ ];
1702
+ }
1703
+
1704
+ // src/utils/schema.ts
1705
+ init_config();
1706
+ import { validateSchema, compareSchemas } from "@basictech/schema";
1707
+ async function getSchemaStatus(schema) {
1708
+ const projectId = schema.project_id;
1709
+ const valid = validateSchema(schema);
1710
+ if (!valid.valid) {
1711
+ console.warn("BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled");
1712
+ return {
1713
+ valid: false,
1714
+ status: "invalid",
1715
+ latest: null
1716
+ };
1717
+ }
1718
+ const latestSchema = await fetch(`https://api.basic.tech/project/${projectId}/schema`).then((res) => res.json()).then((data) => data.data[0].schema).catch((err) => {
1719
+ return {
1720
+ valid: false,
1721
+ status: "error",
1722
+ latest: null
1723
+ };
1724
+ });
1725
+ if (!latestSchema.version) {
1726
+ return {
1727
+ valid: false,
1728
+ status: "error",
1729
+ latest: null
1730
+ };
1731
+ }
1732
+ if (latestSchema.version > schema.version) {
1733
+ console.warn("BasicDB Error: your local schema version is behind the latest. Found version:", schema.version, "but expected", latestSchema.version, " - sync is disabled");
1734
+ return {
1735
+ valid: false,
1736
+ status: "behind",
1737
+ latest: latestSchema
1738
+ };
1739
+ } else if (latestSchema.version < schema.version) {
1740
+ console.warn("BasicDB Error: your local schema version is ahead of the latest. Found version:", schema.version, "but expected", latestSchema.version, " - sync is disabled");
1741
+ return {
1742
+ valid: false,
1743
+ status: "ahead",
1744
+ latest: latestSchema
1745
+ };
1746
+ } else if (latestSchema.version === schema.version) {
1747
+ const changes = compareSchemas(schema, latestSchema);
1748
+ if (changes.valid) {
1749
+ return {
1750
+ valid: true,
1751
+ status: "current",
1752
+ latest: latestSchema
1753
+ };
1754
+ } else {
1755
+ console.warn("BasicDB Error: your local schema is conflicting with the latest. Your version:", schema.version, "does not match origin version", latestSchema.version, " - sync is disabled");
1756
+ return {
1757
+ valid: false,
1758
+ status: "conflict",
1759
+ latest: latestSchema
1760
+ };
1761
+ }
1762
+ } else {
1763
+ return {
1764
+ valid: false,
1765
+ status: "error",
1766
+ latest: null
1767
+ };
1768
+ }
1769
+ }
1770
+ async function validateAndCheckSchema(schema) {
1771
+ const valid = validateSchema(schema);
1772
+ if (!valid.valid) {
1773
+ log("Basic Schema is invalid!", valid.errors);
1774
+ console.group("Schema Errors");
1775
+ let errorMessage = "";
1776
+ valid.errors.forEach((error, index) => {
1777
+ log(`${index + 1}:`, error.message, ` - at ${error.instancePath}`);
1778
+ errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}
1779
+ `;
1780
+ });
1781
+ console.groupEnd();
948
1782
  return {
949
1783
  isValid: false,
950
1784
  schemaStatus: { valid: false },
@@ -956,6 +1790,7 @@ async function validateAndCheckSchema(schema) {
956
1790
  schemaStatus = await getSchemaStatus(schema);
957
1791
  log("schemaStatus", schemaStatus);
958
1792
  } else {
1793
+ schemaStatus = { valid: false, status: "unpublished" };
959
1794
  log("schema not published - at version 0");
960
1795
  }
961
1796
  return {
@@ -968,9 +1803,21 @@ async function validateAndCheckSchema(schema) {
968
1803
  import { jsx, jsxs } from "react/jsx-runtime";
969
1804
  var DEFAULT_AUTH_CONFIG = {
970
1805
  scopes: "profile,email,app:admin",
971
- server_url: "https://api.basic.tech",
1806
+ pds_url: "https://pds.basic.id",
1807
+ admin_url: "https://api.basic.tech",
972
1808
  ws_url: "wss://pds.basic.id/ws"
973
1809
  };
1810
+ var DBStatus = /* @__PURE__ */ ((DBStatus2) => {
1811
+ DBStatus2["LOADING"] = "LOADING";
1812
+ DBStatus2["OFFLINE"] = "OFFLINE";
1813
+ DBStatus2["CONNECTING"] = "CONNECTING";
1814
+ DBStatus2["ONLINE"] = "ONLINE";
1815
+ DBStatus2["SYNCING"] = "SYNCING";
1816
+ DBStatus2["ERROR"] = "ERROR";
1817
+ DBStatus2["ERROR_WILL_RETRY"] = "ERROR_WILL_RETRY";
1818
+ DBStatus2["ERROR_TOKEN_EXPIRED"] = "ERROR_TOKEN_EXPIRED";
1819
+ return DBStatus2;
1820
+ })(DBStatus || {});
974
1821
  var noDb = {
975
1822
  collection: () => {
976
1823
  throw new Error("no basicdb found - initialization failed. double check your schema.");
@@ -981,12 +1828,17 @@ var BasicContext = createContext({
981
1828
  isReady: false,
982
1829
  isSignedIn: false,
983
1830
  user: null,
1831
+ did: null,
1832
+ scope: null,
1833
+ hasScope: () => false,
1834
+ missingScopes: () => [],
984
1835
  // Auth actions
985
1836
  signIn: () => Promise.resolve(),
1837
+ signInWithHandle: () => Promise.resolve(),
986
1838
  signOut: () => Promise.resolve(),
987
1839
  signInWithCode: () => Promise.resolve({ success: false }),
988
1840
  // Token management
989
- getToken: () => Promise.reject(new Error("no token")),
1841
+ getToken: (_options) => Promise.reject(new Error("no token")),
990
1842
  getSignInUrl: () => Promise.resolve(""),
991
1843
  // DB access
992
1844
  db: noDb,
@@ -999,6 +1851,16 @@ var BasicContext = createContext({
999
1851
  signinWithCode: () => Promise.resolve({ success: false }),
1000
1852
  getSignInLink: () => Promise.resolve("")
1001
1853
  });
1854
+ function snapshotAuth(mgr) {
1855
+ return {
1856
+ isSignedIn: mgr.isSignedIn,
1857
+ hasToken: !!mgr.token,
1858
+ isAuthReady: mgr.isAuthReady,
1859
+ user: mgr.user,
1860
+ did: mgr.did,
1861
+ tokenScope: mgr.tokenScope
1862
+ };
1863
+ }
1002
1864
  function BasicProvider({
1003
1865
  children,
1004
1866
  project_id: project_id_prop,
@@ -1009,64 +1871,79 @@ function BasicProvider({
1009
1871
  dbMode = "sync"
1010
1872
  }) {
1011
1873
  const project_id = schema?.project_id || project_id_prop;
1012
- const [isAuthReady, setIsAuthReady] = useState(false);
1013
- const [isSignedIn, setIsSignedIn] = useState(false);
1014
- const [token, setToken] = useState(null);
1015
- const [user, setUser] = useState({});
1016
- const [shouldConnect, setShouldConnect] = useState(false);
1017
- const [isReady, setIsReady] = useState(false);
1018
- const [dbStatus, setDbStatus] = useState("OFFLINE" /* OFFLINE */);
1019
- const [error, setError] = useState(null);
1020
- const [isOnline, setIsOnline] = useState(navigator.onLine);
1021
- const [pendingRefresh, setPendingRefresh] = useState(false);
1022
- const syncRef = useRef(null);
1023
- const remoteDbRef = useRef(null);
1024
- const storageAdapter = storage || new LocalStorageAdapter();
1874
+ if (auth?.server_url && !auth?.pds_url) {
1875
+ log("Warning: auth.server_url is deprecated, use auth.pds_url instead");
1876
+ }
1025
1877
  const authConfig = {
1026
1878
  scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
1027
- server_url: auth?.server_url || DEFAULT_AUTH_CONFIG.server_url,
1879
+ pds_url: auth?.pds_url || auth?.server_url || DEFAULT_AUTH_CONFIG.pds_url,
1880
+ admin_url: auth?.admin_url || DEFAULT_AUTH_CONFIG.admin_url,
1028
1881
  ws_url: auth?.ws_url || DEFAULT_AUTH_CONFIG.ws_url
1029
1882
  };
1030
1883
  const scopesString = Array.isArray(authConfig.scopes) ? authConfig.scopes.join(" ") : authConfig.scopes;
1031
- const refreshPromiseRef = useRef(null);
1884
+ const storageRef = useRef(storage || new LocalStorageAdapter());
1885
+ const storageAdapter = storageRef.current;
1886
+ const [authState, setAuthState] = useState({
1887
+ isSignedIn: false,
1888
+ hasToken: false,
1889
+ isAuthReady: false,
1890
+ user: null,
1891
+ did: null,
1892
+ tokenScope: null
1893
+ });
1894
+ const authRef = useRef(null);
1895
+ if (!authRef.current) {
1896
+ authRef.current = new AuthManager(
1897
+ {
1898
+ projectId: project_id,
1899
+ scopes: scopesString,
1900
+ pdsUrl: authConfig.pds_url,
1901
+ adminUrl: authConfig.admin_url,
1902
+ debug
1903
+ },
1904
+ storageAdapter,
1905
+ () => setAuthState(snapshotAuth(authRef.current))
1906
+ );
1907
+ }
1908
+ const syncRef = useRef(null);
1909
+ const remoteDbRef = useRef(null);
1910
+ const [shouldConnect, setShouldConnect] = useState(false);
1911
+ const [dbStatus, setDbStatus] = useState("OFFLINE" /* OFFLINE */);
1912
+ const [isReady, setIsReady] = useState(false);
1913
+ const [error, setError] = useState(null);
1032
1914
  const isDevMode = () => isDevelopment(debug);
1033
- const cleanOAuthParams = () => cleanOAuthParamsFromUrl();
1034
1915
  useEffect(() => {
1035
- const handleOnline = () => {
1036
- log("Network came back online");
1037
- setIsOnline(true);
1038
- if (pendingRefresh) {
1039
- log("Retrying pending token refresh");
1040
- setPendingRefresh(false);
1041
- if (token) {
1042
- const refreshToken = token.refresh_token || localStorage.getItem("basic_refresh_token");
1043
- if (refreshToken) {
1044
- fetchToken(refreshToken, true).catch((error2) => {
1045
- log("Retry refresh failed:", error2);
1046
- });
1047
- }
1916
+ const runVersionUpdater = async () => {
1917
+ try {
1918
+ const versionUpdater = createVersionUpdater(storageAdapter, version, getMigrations());
1919
+ const updateResult = await versionUpdater.checkAndUpdate();
1920
+ if (updateResult.updated) {
1921
+ log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`);
1922
+ } else {
1923
+ log(`App version ${updateResult.toVersion} is current`);
1048
1924
  }
1925
+ } catch (error2) {
1926
+ log("Version update failed:", error2);
1049
1927
  }
1050
1928
  };
1051
- const handleOffline = () => {
1052
- log("Network went offline");
1053
- setIsOnline(false);
1054
- };
1055
- window.addEventListener("online", handleOnline);
1056
- window.addEventListener("offline", handleOffline);
1057
- return () => {
1058
- window.removeEventListener("online", handleOnline);
1059
- window.removeEventListener("offline", handleOffline);
1060
- };
1061
- }, [pendingRefresh, token]);
1929
+ runVersionUpdater();
1930
+ authRef.current.initialize();
1931
+ return authRef.current.setupNetworkListeners();
1932
+ }, []);
1062
1933
  useEffect(() => {
1063
1934
  async function initSyncDb(options) {
1064
1935
  if (!syncRef.current) {
1065
1936
  log("Initializing Basic Sync DB");
1066
1937
  await initDexieExtensions();
1067
1938
  syncRef.current = new BasicSync("basicdb", { schema });
1068
- syncRef.current.syncable.on("statusChanged", (status, url) => {
1069
- setDbStatus(getSyncStatus(status));
1939
+ syncRef.current.syncable.on("statusChanged", (status) => {
1940
+ const newStatus = getSyncStatus(status);
1941
+ setDbStatus(newStatus);
1942
+ if (newStatus === "ERROR_WILL_RETRY" /* ERROR_WILL_RETRY */) {
1943
+ log("Sync entered ERROR_WILL_RETRY - proactively refreshing token");
1944
+ authRef.current.getToken({ forceRefresh: true }).catch(() => {
1945
+ });
1946
+ }
1070
1947
  });
1071
1948
  if (options.shouldConnect) {
1072
1949
  setShouldConnect(true);
@@ -1089,14 +1966,14 @@ function BasicProvider({
1089
1966
  }
1090
1967
  log("Initializing Basic Remote DB");
1091
1968
  remoteDbRef.current = new RemoteDB({
1092
- serverUrl: authConfig.server_url,
1969
+ serverUrl: authConfig.pds_url,
1093
1970
  projectId: project_id,
1094
- getToken,
1971
+ getToken: (opts) => authRef.current.getToken(opts),
1095
1972
  schema,
1096
1973
  debug,
1097
1974
  onAuthError: (error2) => {
1098
1975
  log("RemoteDB auth error:", error2);
1099
- signout();
1976
+ handleSignOut();
1100
1977
  }
1101
1978
  });
1102
1979
  setDbStatus("ONLINE" /* ONLINE */);
@@ -1127,7 +2004,11 @@ function BasicProvider({
1127
2004
  if (result.schemaStatus.valid) {
1128
2005
  await initSyncDb({ shouldConnect: true });
1129
2006
  } else {
1130
- log("Schema is invalid!", result.schemaStatus);
2007
+ if (result.schemaStatus.status === "unpublished") {
2008
+ log("Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync.");
2009
+ } else {
2010
+ log("Schema is invalid!", result.schemaStatus);
2011
+ }
1131
2012
  await initSyncDb({ shouldConnect: false });
1132
2013
  }
1133
2014
  }
@@ -1144,220 +2025,33 @@ function BasicProvider({
1144
2025
  }
1145
2026
  }, []);
1146
2027
  useEffect(() => {
1147
- async function connectToDb() {
1148
- if (token && syncRef.current && isSignedIn && shouldConnect) {
1149
- const tok = await getToken();
1150
- if (!tok) {
1151
- log("no token found");
1152
- return;
1153
- }
1154
- log("connecting to db...");
1155
- syncRef.current?.connect({
1156
- access_token: tok,
1157
- ws_url: authConfig.ws_url
1158
- }).catch((e) => {
1159
- log("error connecting to db", e);
1160
- });
1161
- }
2028
+ if (authState.hasToken && syncRef.current && authState.isSignedIn && shouldConnect) {
2029
+ log("connecting to db...");
2030
+ syncRef.current?.connect({
2031
+ getToken: (opts) => authRef.current.getToken(opts),
2032
+ ws_url: authConfig.ws_url
2033
+ }).catch((e) => {
2034
+ log("error connecting to db", e);
2035
+ });
1162
2036
  }
1163
- connectToDb();
1164
- }, [isSignedIn, shouldConnect]);
1165
- useEffect(() => {
1166
- const initializeAuth = async () => {
1167
- await storageAdapter.set(STORAGE_KEYS.DEBUG, debug ? "true" : "false");
1168
- const storedServerUrl = await storageAdapter.get(STORAGE_KEYS.SERVER_URL);
1169
- if (storedServerUrl && storedServerUrl !== authConfig.server_url) {
1170
- log("Server URL changed, clearing stored tokens");
1171
- await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
1172
- await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
1173
- await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
1174
- await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
1175
- clearCookie("basic_token");
1176
- clearCookie("basic_access_token");
1177
- }
1178
- await storageAdapter.set(STORAGE_KEYS.SERVER_URL, authConfig.server_url);
1179
- try {
1180
- const versionUpdater = createVersionUpdater(storageAdapter, version, getMigrations());
1181
- const updateResult = await versionUpdater.checkAndUpdate();
1182
- if (updateResult.updated) {
1183
- log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`);
1184
- } else {
1185
- log(`App version ${updateResult.toVersion} is current`);
1186
- }
1187
- } catch (error2) {
1188
- log("Version update failed:", error2);
1189
- }
1190
- try {
1191
- if (window.location.search.includes("code")) {
1192
- let code = window.location?.search?.split("code=")[1]?.split("&")[0];
1193
- if (!code)
1194
- return;
1195
- const state = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE);
1196
- const urlState = window.location.search.split("state=")[1]?.split("&")[0];
1197
- if (!state || state !== urlState) {
1198
- log("error: auth state does not match");
1199
- setIsAuthReady(true);
1200
- await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
1201
- cleanOAuthParams();
1202
- return;
1203
- }
1204
- await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
1205
- cleanOAuthParams();
1206
- fetchToken(code, false).catch((error2) => {
1207
- log("Error fetching token:", error2);
1208
- });
1209
- } else {
1210
- const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
1211
- if (refreshToken) {
1212
- log("Found refresh token in storage, attempting to refresh access token");
1213
- fetchToken(refreshToken, true).catch((error2) => {
1214
- log("Error fetching refresh token:", error2);
1215
- });
1216
- } else {
1217
- let cookie_token = getCookie("basic_token");
1218
- if (cookie_token !== "") {
1219
- const tokenData = JSON.parse(cookie_token);
1220
- setToken(tokenData);
1221
- if (tokenData.refresh_token) {
1222
- await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, tokenData.refresh_token);
1223
- }
1224
- } else {
1225
- const cachedUserInfo = await storageAdapter.get(STORAGE_KEYS.USER_INFO);
1226
- if (cachedUserInfo) {
1227
- try {
1228
- const userData = JSON.parse(cachedUserInfo);
1229
- setUser(userData);
1230
- setIsSignedIn(true);
1231
- log("Loaded cached user info for offline mode");
1232
- } catch (error2) {
1233
- log("Error parsing cached user info:", error2);
1234
- }
1235
- }
1236
- setIsAuthReady(true);
1237
- }
1238
- }
1239
- }
1240
- } catch (e) {
1241
- log("error getting token", e);
1242
- }
1243
- };
1244
- initializeAuth();
1245
- }, []);
1246
- useEffect(() => {
1247
- async function fetchUser(acc_token) {
1248
- console.info("fetching user");
2037
+ }, [authState.isSignedIn, authState.hasToken, shouldConnect]);
2038
+ const handleSignOut = async () => {
2039
+ await authRef.current.signOut();
2040
+ if (syncRef.current) {
1249
2041
  try {
1250
- const response = await fetch(`${authConfig.server_url}/auth/userInfo`, {
1251
- method: "GET",
1252
- headers: {
1253
- "Authorization": `Bearer ${acc_token}`
1254
- }
1255
- });
1256
- if (!response.ok) {
1257
- throw new Error(`Failed to fetch user info: ${response.status}`);
1258
- }
1259
- const user2 = await response.json();
1260
- if (user2.error) {
1261
- log("error fetching user", user2.error);
1262
- throw new Error(`User info error: ${user2.error}`);
1263
- }
1264
- if (token?.refresh_token) {
1265
- await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token);
1266
- }
1267
- await storageAdapter.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user2));
1268
- log("Cached user info in storage");
1269
- setCookie("basic_access_token", token?.access_token || "", { httpOnly: false });
1270
- setCookie("basic_token", JSON.stringify(token));
1271
- setUser(user2);
1272
- setIsSignedIn(true);
1273
- setIsAuthReady(true);
2042
+ await syncRef.current.close();
2043
+ await syncRef.current.delete({ disableAutoOpen: false });
2044
+ syncRef.current = null;
2045
+ window?.location?.reload();
1274
2046
  } catch (error2) {
1275
- log("Failed to fetch user info:", error2);
1276
- setIsAuthReady(true);
1277
- }
1278
- }
1279
- async function checkToken() {
1280
- if (!token) {
1281
- log("error: no user token found");
1282
- setIsAuthReady(true);
1283
- return;
1284
- }
1285
- const decoded = jwtDecode(token?.access_token);
1286
- const expirationBuffer = 5;
1287
- const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
1288
- if (isExpired) {
1289
- log("token is expired - refreshing ...");
1290
- const refreshToken = token?.refresh_token;
1291
- if (!refreshToken) {
1292
- log("Error: No refresh token available for expired token");
1293
- setIsAuthReady(true);
1294
- return;
1295
- }
1296
- try {
1297
- const newToken = await fetchToken(refreshToken, true);
1298
- fetchUser(newToken?.access_token || "");
1299
- } catch (error2) {
1300
- log("Failed to refresh token in checkToken:", error2);
1301
- if (error2.message.includes("offline") || error2.message.includes("Network")) {
1302
- log("Network issue - continuing with expired token until online");
1303
- fetchUser(token?.access_token || "");
1304
- } else {
1305
- setIsAuthReady(true);
1306
- }
1307
- }
1308
- } else {
1309
- fetchUser(token?.access_token || "");
2047
+ console.error("Error during database cleanup:", error2);
1310
2048
  }
1311
2049
  }
1312
- if (token) {
1313
- checkToken();
1314
- }
1315
- }, [token]);
1316
- const getSignInLink = async (redirectUri) => {
1317
- try {
1318
- log("getting sign in link...");
1319
- if (!project_id) {
1320
- throw new Error("Project ID is required to generate sign-in link");
1321
- }
1322
- const randomState = Math.random().toString(36).substring(6);
1323
- await storageAdapter.set(STORAGE_KEYS.AUTH_STATE, randomState);
1324
- const redirectUrl = redirectUri || window.location.href;
1325
- if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
1326
- throw new Error("Invalid redirect URI provided");
1327
- }
1328
- await storageAdapter.set(STORAGE_KEYS.REDIRECT_URI, redirectUrl);
1329
- log("Stored redirect_uri for token exchange:", redirectUrl);
1330
- let baseUrl = `${authConfig.server_url}/auth/authorize`;
1331
- baseUrl += `?client_id=${project_id}`;
1332
- baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`;
1333
- baseUrl += `&response_type=code`;
1334
- baseUrl += `&scope=${encodeURIComponent(scopesString)}`;
1335
- baseUrl += `&state=${randomState}`;
1336
- log("Generated sign-in link successfully with scopes:", scopesString);
1337
- return baseUrl;
1338
- } catch (error2) {
1339
- log("Error generating sign-in link:", error2);
1340
- throw error2;
1341
- }
1342
2050
  };
1343
- const signin = async () => {
2051
+ const handleSignIn = async () => {
1344
2052
  try {
1345
- log("signing in...");
1346
- if (!project_id) {
1347
- log("Error: project_id is required for sign-in");
1348
- throw new Error("Project ID is required for authentication");
1349
- }
1350
- const signInLink = await getSignInLink();
1351
- log("Generated sign-in link:", signInLink);
1352
- try {
1353
- new URL(signInLink);
1354
- } catch {
1355
- log("Error: Invalid sign-in link generated");
1356
- throw new Error("Failed to generate valid sign-in URL");
1357
- }
1358
- window.location.href = signInLink;
2053
+ await authRef.current.signIn();
1359
2054
  } catch (error2) {
1360
- log("Error during sign-in:", error2);
1361
2055
  if (isDevMode()) {
1362
2056
  setError({
1363
2057
  code: "signin_error",
@@ -1368,255 +2062,19 @@ function BasicProvider({
1368
2062
  throw error2;
1369
2063
  }
1370
2064
  };
1371
- const signinWithCode = async (code, state) => {
2065
+ const handleSignInWithHandle = async (handle) => {
1372
2066
  try {
1373
- log("signinWithCode called with code:", code);
1374
- if (!code || typeof code !== "string") {
1375
- return { success: false, error: "Invalid authorization code" };
1376
- }
1377
- if (state) {
1378
- const storedState = await storageAdapter.get(STORAGE_KEYS.AUTH_STATE);
1379
- if (storedState && storedState !== state) {
1380
- log("State parameter mismatch:", { provided: state, stored: storedState });
1381
- return { success: false, error: "State parameter mismatch" };
1382
- }
1383
- }
1384
- await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
1385
- cleanOAuthParams();
1386
- const token2 = await fetchToken(code, false);
1387
- if (token2) {
1388
- log("signinWithCode successful");
1389
- return { success: true };
1390
- } else {
1391
- return { success: false, error: "Failed to exchange code for token" };
1392
- }
2067
+ await authRef.current.signInWithHandle(handle);
1393
2068
  } catch (error2) {
1394
- log("signinWithCode error:", error2);
1395
- return {
1396
- success: false,
1397
- error: error2.message || "Authentication failed"
1398
- };
1399
- }
1400
- };
1401
- const signout = async () => {
1402
- log("signing out!");
1403
- setUser({});
1404
- setIsSignedIn(false);
1405
- setToken(null);
1406
- clearCookie("basic_token");
1407
- clearCookie("basic_access_token");
1408
- await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
1409
- await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
1410
- await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
1411
- await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
1412
- await storageAdapter.remove(STORAGE_KEYS.SERVER_URL);
1413
- if (syncRef.current) {
1414
- (async () => {
1415
- try {
1416
- await syncRef.current?.close();
1417
- await syncRef.current?.delete({ disableAutoOpen: false });
1418
- syncRef.current = null;
1419
- window?.location?.reload();
1420
- } catch (error2) {
1421
- console.error("Error during database cleanup:", error2);
1422
- }
1423
- })();
1424
- }
1425
- };
1426
- const getToken = async () => {
1427
- log("getting token...");
1428
- if (!token) {
1429
- const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
1430
- if (refreshToken) {
1431
- log("No token in memory, attempting to refresh from storage");
1432
- if (refreshPromiseRef.current) {
1433
- log("Token refresh already in progress, waiting...");
1434
- try {
1435
- const newToken = await refreshPromiseRef.current;
1436
- if (newToken?.access_token) {
1437
- return newToken.access_token;
1438
- }
1439
- } catch (error2) {
1440
- log("In-flight refresh failed:", error2);
1441
- throw error2;
1442
- }
1443
- }
1444
- try {
1445
- const newToken = await fetchToken(refreshToken, true);
1446
- if (newToken?.access_token) {
1447
- return newToken.access_token;
1448
- }
1449
- } catch (error2) {
1450
- log("Failed to refresh token from storage:", error2);
1451
- if (error2.message.includes("offline") || error2.message.includes("Network")) {
1452
- log("Network issue - continuing with potentially expired token");
1453
- const lastToken = localStorage.getItem("basic_access_token");
1454
- if (lastToken) {
1455
- return lastToken;
1456
- }
1457
- throw new Error("Network offline - authentication will be retried when online");
1458
- }
1459
- throw new Error("Authentication expired. Please sign in again.");
1460
- }
1461
- }
1462
- log("no token found");
1463
- throw new Error("no token found");
1464
- }
1465
- const decoded = jwtDecode(token?.access_token);
1466
- const expirationBuffer = 5;
1467
- const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
1468
- if (isExpired) {
1469
- log("token is expired - refreshing ...");
1470
- if (refreshPromiseRef.current) {
1471
- log("Token refresh already in progress, waiting...");
1472
- try {
1473
- const newToken = await refreshPromiseRef.current;
1474
- return newToken?.access_token || "";
1475
- } catch (error2) {
1476
- log("In-flight refresh failed:", error2);
1477
- if (error2.message.includes("offline") || error2.message.includes("Network")) {
1478
- log("Network issue - using expired token until network is restored");
1479
- return token.access_token;
1480
- }
1481
- throw error2;
1482
- }
1483
- }
1484
- const refreshToken = token?.refresh_token || await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
1485
- if (refreshToken) {
1486
- try {
1487
- const newToken = await fetchToken(refreshToken, true);
1488
- return newToken?.access_token || "";
1489
- } catch (error2) {
1490
- log("Failed to refresh expired token:", error2);
1491
- if (error2.message.includes("offline") || error2.message.includes("Network")) {
1492
- log("Network issue - using expired token until network is restored");
1493
- return token.access_token;
1494
- }
1495
- throw new Error("Authentication expired. Please sign in again.");
1496
- }
1497
- } else {
1498
- throw new Error("no refresh token available");
1499
- }
1500
- }
1501
- return token?.access_token || "";
1502
- };
1503
- const fetchToken = async (codeOrRefreshToken, isRefreshToken = false) => {
1504
- if (!codeOrRefreshToken || codeOrRefreshToken.trim() === "") {
1505
- const errorMsg = isRefreshToken ? "Refresh token is empty or undefined" : "Authorization code is empty or undefined";
1506
- log("Error:", errorMsg);
1507
- throw new Error(errorMsg);
1508
- }
1509
- if (isRefreshToken && refreshPromiseRef.current) {
1510
- log("Reusing in-flight refresh token request");
1511
- return refreshPromiseRef.current;
1512
- }
1513
- const refreshPromise = (async () => {
1514
- try {
1515
- if (!isOnline) {
1516
- log("Network is offline, marking refresh as pending");
1517
- setPendingRefresh(true);
1518
- throw new Error("Network offline - refresh will be retried when online");
1519
- }
1520
- let requestBody;
1521
- if (isRefreshToken) {
1522
- requestBody = {
1523
- grant_type: "refresh_token",
1524
- refresh_token: codeOrRefreshToken
1525
- };
1526
- if (project_id) {
1527
- requestBody.client_id = project_id;
1528
- }
1529
- } else {
1530
- requestBody = {
1531
- grant_type: "authorization_code",
1532
- code: codeOrRefreshToken
1533
- };
1534
- const storedRedirectUri = await storageAdapter.get(STORAGE_KEYS.REDIRECT_URI);
1535
- if (storedRedirectUri) {
1536
- requestBody.redirect_uri = storedRedirectUri;
1537
- log("Including redirect_uri in token exchange:", storedRedirectUri);
1538
- } else {
1539
- log("Warning: No redirect_uri found in storage for token exchange");
1540
- }
1541
- if (project_id) {
1542
- requestBody.client_id = project_id;
1543
- }
1544
- }
1545
- log("Token exchange request body:", { ...requestBody, refresh_token: isRefreshToken ? "[REDACTED]" : void 0, code: !isRefreshToken ? "[REDACTED]" : void 0 });
1546
- const token2 = await fetch(`${authConfig.server_url}/auth/token`, {
1547
- method: "POST",
1548
- headers: {
1549
- "Content-Type": "application/json"
1550
- },
1551
- body: JSON.stringify(requestBody)
1552
- }).then((response) => response.json()).catch((error2) => {
1553
- log("Network error fetching token:", error2);
1554
- if (!isOnline) {
1555
- setPendingRefresh(true);
1556
- throw new Error("Network offline - refresh will be retried when online");
1557
- }
1558
- throw new Error("Network error during token refresh");
2069
+ if (isDevMode()) {
2070
+ setError({
2071
+ code: "signin_error",
2072
+ title: "Sign-in Failed",
2073
+ message: error2.message || "An error occurred during sign-in. Please try again."
1559
2074
  });
1560
- if (token2.error) {
1561
- log("error fetching token", token2.error);
1562
- if (token2.error.includes("network") || token2.error.includes("timeout")) {
1563
- setPendingRefresh(true);
1564
- throw new Error("Network issue - refresh will be retried when online");
1565
- }
1566
- await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
1567
- await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
1568
- await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
1569
- await storageAdapter.remove(STORAGE_KEYS.SERVER_URL);
1570
- clearCookie("basic_token");
1571
- clearCookie("basic_access_token");
1572
- setUser({});
1573
- setIsSignedIn(false);
1574
- setToken(null);
1575
- setIsAuthReady(true);
1576
- throw new Error(`Token refresh failed: ${token2.error}`);
1577
- } else {
1578
- setToken(token2);
1579
- setPendingRefresh(false);
1580
- if (token2.refresh_token) {
1581
- await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token2.refresh_token);
1582
- log("Updated refresh token in storage");
1583
- }
1584
- if (!isRefreshToken) {
1585
- await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
1586
- log("Cleaned up redirect_uri from storage after successful exchange");
1587
- }
1588
- setCookie("basic_access_token", token2.access_token, { httpOnly: false });
1589
- setCookie("basic_token", JSON.stringify(token2));
1590
- log("Updated access token and full token in cookies");
1591
- }
1592
- return token2;
1593
- } catch (error2) {
1594
- log("Token refresh error:", error2);
1595
- if (!error2.message.includes("offline") && !error2.message.includes("Network")) {
1596
- await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
1597
- await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
1598
- await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
1599
- await storageAdapter.remove(STORAGE_KEYS.SERVER_URL);
1600
- clearCookie("basic_token");
1601
- clearCookie("basic_access_token");
1602
- setUser({});
1603
- setIsSignedIn(false);
1604
- setToken(null);
1605
- setIsAuthReady(true);
1606
- }
1607
- throw error2;
1608
2075
  }
1609
- })();
1610
- if (isRefreshToken) {
1611
- refreshPromiseRef.current = refreshPromise;
1612
- refreshPromise.finally(() => {
1613
- if (refreshPromiseRef.current === refreshPromise) {
1614
- refreshPromiseRef.current = null;
1615
- log("Cleared refresh promise reference");
1616
- }
1617
- });
2076
+ throw error2;
1618
2077
  }
1619
- return refreshPromise;
1620
2078
  };
1621
2079
  const getCurrentDb = () => {
1622
2080
  if (dbMode === "remote") {
@@ -1625,27 +2083,32 @@ function BasicProvider({
1625
2083
  return syncRef.current || noDb;
1626
2084
  };
1627
2085
  const contextValue = {
1628
- // Auth state (new naming)
1629
- isReady: isAuthReady,
1630
- isSignedIn,
1631
- user,
1632
- // Auth actions (new camelCase naming)
1633
- signIn: signin,
1634
- signOut: signout,
1635
- signInWithCode: signinWithCode,
2086
+ // Auth state
2087
+ isReady: authState.isAuthReady,
2088
+ isSignedIn: authState.isSignedIn,
2089
+ user: authState.user,
2090
+ did: authState.did,
2091
+ scope: authState.tokenScope,
2092
+ hasScope: (scope) => authRef.current.hasScope(scope),
2093
+ missingScopes: () => authRef.current.missingScopes(),
2094
+ // Auth actions
2095
+ signIn: handleSignIn,
2096
+ signInWithHandle: handleSignInWithHandle,
2097
+ signOut: handleSignOut,
2098
+ signInWithCode: (code, state) => authRef.current.signInWithCode(code, state),
1636
2099
  // Token management
1637
- getToken,
1638
- getSignInUrl: getSignInLink,
2100
+ getToken: (opts) => authRef.current.getToken(opts),
2101
+ getSignInUrl: (redirectUri) => authRef.current.getSignInUrl(redirectUri),
1639
2102
  // DB access
1640
2103
  db: getCurrentDb(),
1641
2104
  dbStatus,
1642
2105
  dbMode,
1643
2106
  // Legacy aliases (deprecated)
1644
- isAuthReady,
1645
- signin,
1646
- signout,
1647
- signinWithCode,
1648
- getSignInLink
2107
+ isAuthReady: authState.isAuthReady,
2108
+ signin: handleSignIn,
2109
+ signout: handleSignOut,
2110
+ signinWithCode: (code, state) => authRef.current.signInWithCode(code, state),
2111
+ getSignInLink: (redirectUri) => authRef.current.getSignInUrl(redirectUri)
1649
2112
  };
1650
2113
  return /* @__PURE__ */ jsxs(BasicContext.Provider, { value: contextValue, children: [
1651
2114
  error && isDevMode() && /* @__PURE__ */ jsx(ErrorDisplay, { error }),
@@ -1683,11 +2146,15 @@ function useBasic() {
1683
2146
  import { useLiveQuery as useQuery } from "dexie-react-hooks";
1684
2147
  export {
1685
2148
  BasicProvider,
2149
+ DBStatus,
1686
2150
  NotAuthenticatedError,
1687
2151
  RemoteCollection,
1688
2152
  RemoteDB,
1689
2153
  RemoteDBError,
1690
2154
  STORAGE_KEYS,
2155
+ resolveDid,
2156
+ resolveDidWebUrl,
2157
+ resolveHandle,
1691
2158
  useBasic,
1692
2159
  useQuery
1693
2160
  };