@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.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();
@@ -166,37 +239,38 @@ var init_syncProtocol = __esm({
166
239
  });
167
240
 
168
241
  // src/index.ts
169
- var src_exports = {};
170
- __export(src_exports, {
242
+ var index_exports = {};
243
+ __export(index_exports, {
171
244
  BasicProvider: () => BasicProvider,
245
+ DBStatus: () => DBStatus,
172
246
  NotAuthenticatedError: () => NotAuthenticatedError,
173
247
  RemoteCollection: () => RemoteCollection,
174
248
  RemoteDB: () => RemoteDB,
175
249
  RemoteDBError: () => RemoteDBError,
176
250
  STORAGE_KEYS: () => STORAGE_KEYS,
251
+ resolveDid: () => resolveDid,
252
+ resolveDidWebUrl: () => resolveDidWebUrl,
253
+ resolveHandle: () => resolveHandle,
177
254
  useBasic: () => useBasic,
178
255
  useQuery: () => import_dexie_react_hooks.useLiveQuery
179
256
  });
180
- module.exports = __toCommonJS(src_exports);
257
+ module.exports = __toCommonJS(index_exports);
181
258
 
182
259
  // src/AuthContext.tsx
183
260
  var import_react = require("react");
184
- var import_jwt_decode = require("jwt-decode");
185
261
 
186
262
  // src/sync/index.ts
187
263
  var import_uuid = require("uuid");
188
264
  var import_dexie2 = require("dexie");
189
265
  init_config();
190
266
  var import_schema = require("@basictech/schema");
267
+ init_tokenRegistry();
191
268
  var dexieExtensionsLoaded = false;
192
269
  var initPromise = null;
193
270
  async function initDexieExtensions() {
194
- if (dexieExtensionsLoaded)
195
- return;
196
- if (typeof window === "undefined")
197
- return;
198
- if (initPromise)
199
- return initPromise;
271
+ if (dexieExtensionsLoaded) return;
272
+ if (typeof window === "undefined") return;
273
+ if (initPromise) return initPromise;
200
274
  initPromise = (async () => {
201
275
  try {
202
276
  await import("dexie-syncable");
@@ -221,12 +295,13 @@ var BasicSync = class extends import_dexie2.Dexie {
221
295
  this.version(2).stores({});
222
296
  this.Collection.prototype.get = this.Collection.prototype.toArray;
223
297
  }
224
- async connect({ access_token, ws_url }) {
298
+ async connect({ getToken, ws_url }) {
225
299
  const WS_URL = ws_url || "wss://pds.basic.id/ws";
226
300
  log("Connecting to", WS_URL);
301
+ setTokenGetter(WS_URL, getToken);
227
302
  await this.updateSyncNodes();
228
303
  log("Starting connection...");
229
- return this.syncable.connect("websocket", WS_URL, { authToken: access_token, schema: this.basic_schema });
304
+ return this.syncable.connect("websocket", WS_URL, { schema: this.basic_schema });
230
305
  }
231
306
  async disconnect({ ws_url } = {}) {
232
307
  const WS_URL = ws_url || "wss://pds.basic.id/ws";
@@ -266,7 +341,7 @@ var BasicSync = class extends import_dexie2.Dexie {
266
341
  }
267
342
  _convertSchemaToDxSchema(schema) {
268
343
  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("");
344
+ const indexedFields = Object.entries(table.fields).filter(([, field]) => field.indexed).map(([fieldKey]) => `,${fieldKey}`).join("");
270
345
  return {
271
346
  [key]: "id" + indexedFields
272
347
  };
@@ -433,29 +508,45 @@ var RemoteCollection = class {
433
508
  const token = await this.config.getToken();
434
509
  const url = `${this.config.serverUrl}${path}`;
435
510
  this.log(`${method} ${url}`, body ? JSON.stringify(body) : "");
511
+ const headers = {
512
+ "Authorization": `Bearer ${token}`
513
+ };
514
+ if (body) {
515
+ headers["Content-Type"] = "application/json";
516
+ }
436
517
  const response = await fetch(url, {
437
518
  method,
438
- headers: {
439
- "Content-Type": "application/json",
440
- "Authorization": `Bearer ${token}`
441
- },
519
+ headers,
442
520
  ...body ? { body: JSON.stringify(body) } : {}
443
521
  });
444
522
  const responseData = await response.json().catch(() => ({}));
445
523
  if (!response.ok) {
446
524
  if (response.status === 401 && !isRetry) {
447
- this.log("Got 401, retrying with fresh token...");
525
+ this.log("Got 401, forcing token refresh and retrying...");
526
+ await this.config.getToken({ forceRefresh: true });
448
527
  return this.request(method, path, body, true);
449
528
  }
450
529
  if (this.config.debug) {
451
530
  console.error(`[RemoteDB] Error ${response.status}:`, responseData);
452
531
  }
453
- if (response.status === 401 && this.config.onAuthError) {
454
- this.config.onAuthError({
455
- status: response.status,
456
- message: "Authentication failed",
457
- response: responseData
458
- });
532
+ if (this.config.onAuthError) {
533
+ if (response.status === 401) {
534
+ this.config.onAuthError({
535
+ status: response.status,
536
+ message: "Authentication failed",
537
+ response: responseData,
538
+ errorType: "expired",
539
+ afterRetry: isRetry
540
+ });
541
+ } else if (response.status === 403) {
542
+ this.config.onAuthError({
543
+ status: response.status,
544
+ message: responseData.message || "Forbidden - insufficient permissions or missing scope",
545
+ response: responseData,
546
+ errorType: "forbidden",
547
+ afterRetry: isRetry
548
+ });
549
+ }
459
550
  }
460
551
  const errorMessage = responseData.message || responseData.error || responseData.detail || (typeof responseData === "string" ? responseData : `API request failed: ${response.status}`);
461
552
  throw new RemoteDBError(errorMessage, response.status, responseData);
@@ -654,136 +745,8 @@ var RemoteDB = class {
654
745
  }
655
746
  };
656
747
 
657
- // src/AuthContext.tsx
658
- init_config();
659
-
660
- // package.json
661
- var version = "0.7.0-beta.6";
662
-
663
- // src/updater/versionUpdater.ts
664
- var VersionUpdater = class {
665
- storage;
666
- currentVersion;
667
- migrations;
668
- versionKey = "basic_app_version";
669
- constructor(storage, currentVersion, migrations = []) {
670
- this.storage = storage;
671
- this.currentVersion = currentVersion;
672
- this.migrations = migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
673
- }
674
- /**
675
- * Check current stored version and run migrations if needed
676
- * Only compares major.minor versions, ignoring beta/prerelease parts
677
- * Example: "0.7.0-beta.1" and "0.7.0" are treated as the same version
678
- */
679
- async checkAndUpdate() {
680
- const storedVersion = await this.getStoredVersion();
681
- if (!storedVersion) {
682
- await this.setStoredVersion(this.currentVersion);
683
- return { updated: false, toVersion: this.currentVersion };
684
- }
685
- if (storedVersion === this.currentVersion) {
686
- return { updated: false, toVersion: this.currentVersion };
687
- }
688
- const migrationsToRun = this.getMigrationsToRun(storedVersion, this.currentVersion);
689
- if (migrationsToRun.length === 0) {
690
- await this.setStoredVersion(this.currentVersion);
691
- return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion };
692
- }
693
- for (const migration of migrationsToRun) {
694
- try {
695
- console.log(`Running migration from ${migration.fromVersion} to ${migration.toVersion}`);
696
- await migration.migrate(this.storage);
697
- } catch (error) {
698
- console.error(`Migration failed from ${migration.fromVersion} to ${migration.toVersion}:`, error);
699
- throw new Error(`Migration failed: ${error}`);
700
- }
701
- }
702
- await this.setStoredVersion(this.currentVersion);
703
- return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion };
704
- }
705
- async getStoredVersion() {
706
- try {
707
- const versionData = await this.storage.get(this.versionKey);
708
- if (!versionData)
709
- return null;
710
- const versionInfo = JSON.parse(versionData);
711
- return versionInfo.version;
712
- } catch (error) {
713
- console.warn("Failed to get stored version:", error);
714
- return null;
715
- }
716
- }
717
- async setStoredVersion(version2) {
718
- const versionInfo = {
719
- version: version2,
720
- lastUpdated: Date.now()
721
- };
722
- await this.storage.set(this.versionKey, JSON.stringify(versionInfo));
723
- }
724
- getMigrationsToRun(fromVersion, toVersion) {
725
- return this.migrations.filter((migration) => {
726
- const storedLessThanMigrationTo = this.compareVersions(fromVersion, migration.toVersion) < 0;
727
- 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
- const shouldRun = storedLessThanMigrationTo && currentGreaterThanOrEqualMigrationTo;
732
- console.log(` Should run: ${shouldRun}`);
733
- return shouldRun;
734
- });
735
- }
736
- /**
737
- * Simple semantic version comparison (major.minor only, ignoring beta/prerelease)
738
- * Returns: -1 if a < b, 0 if a === b, 1 if a > b
739
- */
740
- compareVersions(a, b) {
741
- const aMajorMinor = this.extractMajorMinor(a);
742
- const bMajorMinor = this.extractMajorMinor(b);
743
- if (aMajorMinor.major !== bMajorMinor.major) {
744
- return aMajorMinor.major - bMajorMinor.major;
745
- }
746
- return aMajorMinor.minor - bMajorMinor.minor;
747
- }
748
- /**
749
- * Extract major.minor from version string, ignoring beta/prerelease
750
- * Examples: "0.7.0-beta.1" -> {major: 0, minor: 7}
751
- * "1.2.3" -> {major: 1, minor: 2}
752
- */
753
- extractMajorMinor(version2) {
754
- const cleanVersion = version2.split("-")[0]?.split("+")[0] || version2;
755
- const parts = cleanVersion.split(".").map(Number);
756
- return {
757
- major: parts[0] || 0,
758
- minor: parts[1] || 0
759
- };
760
- }
761
- /**
762
- * Add a migration to the updater
763
- */
764
- addMigration(migration) {
765
- this.migrations.push(migration);
766
- this.migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
767
- }
768
- };
769
- function createVersionUpdater(storage, currentVersion, migrations = []) {
770
- return new VersionUpdater(storage, currentVersion, migrations);
771
- }
772
-
773
- // src/updater/updateMigrations.ts
774
- var addMigrationTimestamp = {
775
- fromVersion: "0.6.0",
776
- toVersion: "0.7.0",
777
- async migrate(storage) {
778
- console.log("Running test migration");
779
- storage.set("test_migration", "true");
780
- }
781
- };
782
- function getMigrations() {
783
- return [
784
- addMigrationTimestamp
785
- ];
786
- }
748
+ // src/core/auth/AuthManager.ts
749
+ var import_jwt_decode = require("jwt-decode");
787
750
 
788
751
  // src/utils/storage.ts
789
752
  var LocalStorageAdapter = class {
@@ -803,44 +766,95 @@ var STORAGE_KEYS = {
803
766
  AUTH_STATE: "basic_auth_state",
804
767
  REDIRECT_URI: "basic_redirect_uri",
805
768
  SERVER_URL: "basic_server_url",
806
- DEBUG: "basic_debug"
769
+ PDS_ENDPOINTS: "basic_pds_endpoints",
770
+ LAST_CONNECT_REPORT: "basic_last_connect_report",
771
+ DEBUG: "basic_debug",
772
+ CODE_VERIFIER: "basic_code_verifier"
807
773
  };
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
- }
774
+
775
+ // src/utils/normalizeClientId.ts
776
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
777
+ function normalizeClientId(projectId, adminHostname = "api.basic.tech") {
778
+ if (!projectId) return projectId;
779
+ if (projectId === "self") return projectId;
780
+ if (projectId.startsWith("did:")) return projectId;
781
+ if (UUID_RE.test(projectId)) {
782
+ const hex = projectId.replace(/-/g, "").toLowerCase();
783
+ return `did:web:${adminHostname}:projects:${hex}`;
784
+ }
785
+ return projectId;
786
+ }
787
+
788
+ // src/utils/resolveDid.ts
789
+ function resolveDidWebUrl(did) {
790
+ if (!did.startsWith("did:web:")) return null;
791
+ const rest = did.slice(8);
792
+ if (!rest) return null;
793
+ const parts = rest.split(":");
794
+ const hostname = parts[0].replace(/%3A/gi, ":");
795
+ if (parts.length === 1) {
796
+ return `https://${hostname}/.well-known/did.json`;
819
797
  }
820
- return cookieValue;
798
+ const pathParts = parts.slice(1).map((p) => decodeURIComponent(p));
799
+ return `https://${hostname}/${pathParts.join("/")}/did.json`;
821
800
  }
822
- function setCookie(name, value, options) {
823
- const opts = {
824
- secure: true,
825
- sameSite: "Strict",
826
- httpOnly: false,
827
- ...options
801
+ async function resolveFromDocument(did, didDocument) {
802
+ const services = didDocument.service;
803
+ const pdsService = services?.find(
804
+ (s) => s.id === "#basic_pds" || s.id === `${did}#basic_pds`
805
+ );
806
+ if (!pdsService) {
807
+ throw new Error(`DID document has no #basic_pds service entry`);
808
+ }
809
+ const pdsUrl = pdsService.serviceEndpoint.replace(/\/+$/, "");
810
+ const oauthRes = await fetch(`${pdsUrl}/auth/.well-known/openid-configuration`);
811
+ if (!oauthRes.ok) {
812
+ throw new Error(`Failed to fetch OpenID configuration from ${pdsUrl}: ${oauthRes.status}`);
813
+ }
814
+ const oauth = await oauthRes.json();
815
+ return {
816
+ did,
817
+ didDocument,
818
+ pdsUrl,
819
+ authorization_endpoint: oauth.authorization_endpoint,
820
+ token_endpoint: oauth.token_endpoint,
821
+ userinfo_endpoint: oauth.userinfo_endpoint
828
822
  };
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
823
  }
838
- function clearCookie(name) {
839
- document.cookie = `${name}=; Secure; SameSite=Strict`;
824
+ async function resolveDid(did) {
825
+ const url = resolveDidWebUrl(did);
826
+ if (!url) {
827
+ throw new Error(`Unsupported DID method: ${did}`);
828
+ }
829
+ const didRes = await fetch(url);
830
+ if (!didRes.ok) {
831
+ throw new Error(`Failed to fetch DID document at ${url}: ${didRes.status}`);
832
+ }
833
+ const didDocument = await didRes.json();
834
+ return resolveFromDocument(did, didDocument);
835
+ }
836
+ async function resolveHandle(handle) {
837
+ const res = await fetch(`https://${handle}/.well-known/did.json`);
838
+ if (!res.ok) {
839
+ throw new Error(`Handle resolution failed for ${handle}: ${res.status}`);
840
+ }
841
+ const didDocument = await res.json();
842
+ const did = didDocument.id;
843
+ if (!did) {
844
+ throw new Error(`Handle response has no 'id' field`);
845
+ }
846
+ const resolved = await resolveFromDocument(did, didDocument);
847
+ resolved.handle = handle;
848
+ return resolved;
840
849
  }
841
850
 
842
851
  // src/utils/network.ts
843
852
  init_config();
853
+
854
+ // package.json
855
+ var version = "0.8.0-beta.1";
856
+
857
+ // src/utils/network.ts
844
858
  function isDevelopment(debug) {
845
859
  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
860
  }
@@ -902,81 +916,905 @@ function getSyncStatus(statusCode) {
902
916
  }
903
917
  }
904
918
 
905
- // src/utils/schema.ts
906
- var import_schema3 = require("@basictech/schema");
919
+ // src/core/auth/AuthManager.ts
907
920
  init_config();
908
- async function getSchemaStatus(schema) {
909
- const projectId = schema.project_id;
910
- const valid = (0, import_schema3.validateSchema)(schema);
911
- if (!valid.valid) {
912
- console.warn("BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled");
913
- return {
914
- valid: false,
915
- status: "invalid",
916
- latest: null
917
- };
921
+ function generateCodeVerifier() {
922
+ const array = new Uint8Array(32);
923
+ crypto.getRandomValues(array);
924
+ return base64UrlEncode(array);
925
+ }
926
+ async function generateCodeChallenge(verifier) {
927
+ if (typeof crypto === "undefined" || !crypto.subtle) {
928
+ log("crypto.subtle unavailable (non-secure context?) -- falling back to plain PKCE challenge");
929
+ return { challenge: verifier, method: "plain" };
918
930
  }
919
- const latestSchema = await fetch(`https://api.basic.tech/project/${projectId}/schema`).then((res) => res.json()).then((data) => data.data[0].schema).catch((err) => {
920
- return {
921
- valid: false,
922
- status: "error",
923
- latest: null
924
- };
925
- });
926
- console.log("latestSchema", latestSchema);
927
- if (!latestSchema.version) {
928
- return {
929
- valid: false,
930
- status: "error",
931
- latest: null
932
- };
931
+ const encoder = new TextEncoder();
932
+ const data = encoder.encode(verifier);
933
+ const digest = await crypto.subtle.digest("SHA-256", data);
934
+ return { challenge: base64UrlEncode(new Uint8Array(digest)), method: "S256" };
935
+ }
936
+ function base64UrlEncode(buffer) {
937
+ let str = "";
938
+ for (let i = 0; i < buffer.length; i++) {
939
+ str += String.fromCharCode(buffer[i]);
933
940
  }
934
- if (latestSchema.version > schema.version) {
935
- console.warn("BasicDB Error: your local schema version is behind the latest. Found version:", schema.version, "but expected", latestSchema.version, " - sync is disabled");
936
- return {
937
- valid: false,
938
- status: "behind",
939
- latest: latestSchema
940
- };
941
- } else if (latestSchema.version < schema.version) {
942
- console.warn("BasicDB Error: your local schema version is ahead of the latest. Found version:", schema.version, "but expected", latestSchema.version, " - sync is disabled");
943
- return {
944
- valid: false,
945
- status: "ahead",
946
- latest: latestSchema
947
- };
948
- } else if (latestSchema.version === schema.version) {
949
- const changes = (0, import_schema3.compareSchemas)(schema, latestSchema);
950
- if (changes.valid) {
951
- return {
952
- valid: true,
953
- status: "current",
954
- latest: latestSchema
955
- };
956
- } else {
957
- 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");
958
- return {
959
- valid: false,
960
- status: "conflict",
961
- latest: latestSchema
941
+ return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
942
+ }
943
+ var AuthManager = class {
944
+ // --- Public state (read by the UI layer) ---
945
+ token = null;
946
+ user = null;
947
+ isSignedIn = false;
948
+ isAuthReady = false;
949
+ did = null;
950
+ /** Space-separated scopes granted in the current access token */
951
+ tokenScope = null;
952
+ /** Space-separated scopes originally requested in the auth config */
953
+ requestedScopes;
954
+ config;
955
+ storage;
956
+ /** True only during a user-initiated OAuth code exchange (not session restore) */
957
+ freshSignIn = false;
958
+ // --- Private ---
959
+ notify;
960
+ refreshPromise = null;
961
+ codeExchangePromise = null;
962
+ pendingRefresh = false;
963
+ isOnline = typeof navigator !== "undefined" ? navigator.onLine : true;
964
+ channel = null;
965
+ constructor(config, storage, notify) {
966
+ this.config = config;
967
+ this.storage = storage;
968
+ this.notify = notify;
969
+ this.requestedScopes = config.scopes;
970
+ this.initCrossTabSync();
971
+ }
972
+ initCrossTabSync() {
973
+ if (typeof BroadcastChannel === "undefined") return;
974
+ try {
975
+ this.channel = new BroadcastChannel("basic-auth");
976
+ this.channel.onmessage = (event) => {
977
+ if (event.data?.type === "token_refreshed") {
978
+ log("Received token refresh from another tab");
979
+ if (event.data.accessToken && this.token) {
980
+ this.token = { ...this.token, access_token: event.data.accessToken };
981
+ }
982
+ if (event.data.did) this.did = event.data.did;
983
+ if (event.data.tokenScope) this.tokenScope = event.data.tokenScope;
984
+ this.notify();
985
+ }
986
+ if (event.data?.type === "signed_in") {
987
+ log("Received sign-in from another tab, reloading");
988
+ if (typeof window !== "undefined") {
989
+ window.location.reload();
990
+ }
991
+ }
992
+ if (event.data?.type === "signed_out") {
993
+ log("Received sign-out from another tab, reloading");
994
+ this.user = null;
995
+ this.isSignedIn = false;
996
+ this.token = null;
997
+ this.did = null;
998
+ this.tokenScope = null;
999
+ this.notify();
1000
+ if (typeof window !== "undefined") {
1001
+ window.location.reload();
1002
+ }
1003
+ }
962
1004
  };
1005
+ } catch {
1006
+ log("BroadcastChannel not available for cross-tab sync");
963
1007
  }
964
- } else {
965
- return {
966
- valid: false,
967
- status: "error",
968
- latest: null
969
- };
970
1008
  }
971
- }
972
- async function validateAndCheckSchema(schema) {
973
- const valid = (0, import_schema3.validateSchema)(schema);
974
- if (!valid.valid) {
975
- log("Basic Schema is invalid!", valid.errors);
976
- console.group("Schema Errors");
977
- let errorMessage = "";
978
- valid.errors.forEach((error, index) => {
979
- log(`${index + 1}:`, error.message, ` - at ${error.instancePath}`);
1009
+ broadcastTokenRefresh() {
1010
+ this.channel?.postMessage({
1011
+ type: "token_refreshed",
1012
+ accessToken: this.token?.access_token,
1013
+ did: this.did,
1014
+ tokenScope: this.tokenScope
1015
+ });
1016
+ }
1017
+ broadcastSignIn() {
1018
+ this.channel?.postMessage({ type: "signed_in" });
1019
+ }
1020
+ broadcastSignOut() {
1021
+ this.channel?.postMessage({ type: "signed_out" });
1022
+ }
1023
+ // ------------------------------------------------------------------
1024
+ // Public API
1025
+ // ------------------------------------------------------------------
1026
+ /**
1027
+ * Bootstrap auth: handle OAuth callback (?code=), restore session
1028
+ * from refresh token, or load cached user for offline mode.
1029
+ */
1030
+ async initialize() {
1031
+ await this.storage.set(STORAGE_KEYS.DEBUG, this.config.debug ? "true" : "false");
1032
+ const storedServerUrl = await this.storage.get(STORAGE_KEYS.SERVER_URL);
1033
+ if (storedServerUrl && storedServerUrl !== this.config.pdsUrl) {
1034
+ log("PDS URL changed, clearing stored tokens");
1035
+ await this.clearStoredAuth();
1036
+ }
1037
+ await this.storage.set(STORAGE_KEYS.SERVER_URL, this.config.pdsUrl);
1038
+ try {
1039
+ const params = new URLSearchParams(window.location.search);
1040
+ if (params.has("code")) {
1041
+ const code = params.get("code");
1042
+ if (!code) {
1043
+ this.isAuthReady = true;
1044
+ this.notify();
1045
+ return;
1046
+ }
1047
+ const state = await this.storage.get(STORAGE_KEYS.AUTH_STATE);
1048
+ const urlState = params.get("state");
1049
+ if (!state || state !== urlState) {
1050
+ log("error: auth state does not match");
1051
+ this.isAuthReady = true;
1052
+ this.notify();
1053
+ await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
1054
+ cleanOAuthParamsFromUrl();
1055
+ return;
1056
+ }
1057
+ await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
1058
+ cleanOAuthParamsFromUrl();
1059
+ this.freshSignIn = true;
1060
+ this.exchangeToken(code, false).catch((error) => {
1061
+ log("Error fetching token:", error);
1062
+ });
1063
+ } else {
1064
+ const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1065
+ if (refreshToken) {
1066
+ log("Found refresh token in storage, attempting to refresh access token");
1067
+ this.exchangeToken(refreshToken, true).catch((error) => {
1068
+ log("Error fetching refresh token:", error);
1069
+ });
1070
+ } else {
1071
+ const cachedUserInfo = await this.storage.get(STORAGE_KEYS.USER_INFO);
1072
+ if (cachedUserInfo) {
1073
+ try {
1074
+ this.user = JSON.parse(cachedUserInfo);
1075
+ this.isSignedIn = true;
1076
+ log("Loaded cached user info for offline mode");
1077
+ } catch (error) {
1078
+ log("Error parsing cached user info:", error);
1079
+ }
1080
+ }
1081
+ this.isAuthReady = true;
1082
+ this.notify();
1083
+ }
1084
+ }
1085
+ } catch (e) {
1086
+ log("error getting token", e);
1087
+ }
1088
+ }
1089
+ /**
1090
+ * Get a valid access token string. Refreshes proactively (5s buffer)
1091
+ * or on demand (forceRefresh). Mutex prevents concurrent refreshes.
1092
+ */
1093
+ async getToken(options) {
1094
+ log("getting token...");
1095
+ if (!this.token) {
1096
+ const refreshToken = await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1097
+ if (refreshToken) {
1098
+ log("No token in memory, attempting to refresh from storage");
1099
+ if (this.refreshPromise) {
1100
+ log("Token refresh already in progress, waiting...");
1101
+ try {
1102
+ const newToken = await this.refreshPromise;
1103
+ if (newToken?.access_token) {
1104
+ return newToken.access_token;
1105
+ }
1106
+ } catch (error) {
1107
+ log("In-flight refresh failed:", error);
1108
+ throw error;
1109
+ }
1110
+ }
1111
+ try {
1112
+ const newToken = await this.exchangeToken(refreshToken, true);
1113
+ if (newToken?.access_token) {
1114
+ return newToken.access_token;
1115
+ }
1116
+ } catch (error) {
1117
+ log("Failed to refresh token from storage:", error);
1118
+ if (this.isNetworkError(error)) {
1119
+ throw new Error("Network offline - authentication will be retried when online");
1120
+ }
1121
+ throw new Error("Authentication expired. Please sign in again.");
1122
+ }
1123
+ }
1124
+ log("no token found");
1125
+ throw new Error("no token found");
1126
+ }
1127
+ const decoded = (0, import_jwt_decode.jwtDecode)(this.token.access_token);
1128
+ const expirationBuffer = 5;
1129
+ const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
1130
+ const shouldRefresh = isExpired || options?.forceRefresh === true;
1131
+ if (shouldRefresh) {
1132
+ log(options?.forceRefresh ? "force refreshing token..." : "token is expired - refreshing ...");
1133
+ if (this.refreshPromise) {
1134
+ log("Token refresh already in progress, waiting...");
1135
+ try {
1136
+ const newToken = await this.refreshPromise;
1137
+ return newToken?.access_token || "";
1138
+ } catch (error) {
1139
+ log("In-flight refresh failed:", error);
1140
+ if (this.isNetworkError(error)) {
1141
+ log("Network issue - using expired token until network is restored");
1142
+ return this.token.access_token;
1143
+ }
1144
+ throw error;
1145
+ }
1146
+ }
1147
+ const refreshToken = this.token.refresh_token || await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1148
+ if (refreshToken) {
1149
+ try {
1150
+ const newToken = await this.exchangeToken(refreshToken, true);
1151
+ return newToken?.access_token || "";
1152
+ } catch (error) {
1153
+ log("Failed to refresh expired token:", error);
1154
+ if (this.isNetworkError(error)) {
1155
+ log("Network issue - using expired token until network is restored");
1156
+ return this.token.access_token;
1157
+ }
1158
+ throw new Error("Authentication expired. Please sign in again.");
1159
+ }
1160
+ } else {
1161
+ throw new Error("no refresh token available");
1162
+ }
1163
+ }
1164
+ return this.token.access_token || "";
1165
+ }
1166
+ async getSignInUrl(redirectUri, endpoints) {
1167
+ log("getting sign in link...");
1168
+ if (!this.config.projectId) {
1169
+ throw new Error("Project ID is required to generate sign-in link");
1170
+ }
1171
+ const pdsEndpoints = endpoints || this.defaultPdsEndpoints();
1172
+ await this.storage.set(STORAGE_KEYS.PDS_ENDPOINTS, JSON.stringify(pdsEndpoints));
1173
+ const randomState = Math.random().toString(36).substring(6);
1174
+ await this.storage.set(STORAGE_KEYS.AUTH_STATE, randomState);
1175
+ const redirectUrl = redirectUri || window.location.href;
1176
+ if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
1177
+ throw new Error("Invalid redirect URI provided");
1178
+ }
1179
+ await this.storage.set(STORAGE_KEYS.REDIRECT_URI, redirectUrl);
1180
+ log("Stored redirect_uri for token exchange:", redirectUrl);
1181
+ const codeVerifier = generateCodeVerifier();
1182
+ const { challenge: codeChallenge, method: challengeMethod } = await generateCodeChallenge(codeVerifier);
1183
+ await this.storage.set(STORAGE_KEYS.CODE_VERIFIER, codeVerifier);
1184
+ let baseUrl = pdsEndpoints.authorization_endpoint;
1185
+ baseUrl += `?client_id=${encodeURIComponent(normalizeClientId(this.config.projectId, this.adminHostname))}`;
1186
+ baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`;
1187
+ baseUrl += `&response_type=code`;
1188
+ baseUrl += `&scope=${encodeURIComponent(this.config.scopes)}`;
1189
+ baseUrl += `&state=${randomState}`;
1190
+ baseUrl += `&code_challenge=${encodeURIComponent(codeChallenge)}`;
1191
+ baseUrl += `&code_challenge_method=${challengeMethod}`;
1192
+ log("Generated sign-in link successfully with scopes:", this.config.scopes);
1193
+ return baseUrl;
1194
+ }
1195
+ async signIn(redirectUri) {
1196
+ log("signing in...");
1197
+ if (!this.config.projectId) {
1198
+ log("Error: project_id is required for sign-in");
1199
+ throw new Error("Project ID is required for authentication");
1200
+ }
1201
+ const signInLink = await this.getSignInUrl(redirectUri);
1202
+ log("Generated sign-in link:", signInLink);
1203
+ try {
1204
+ new URL(signInLink);
1205
+ } catch {
1206
+ log("Error: Invalid sign-in link generated");
1207
+ throw new Error("Failed to generate valid sign-in URL");
1208
+ }
1209
+ window.location.href = signInLink;
1210
+ }
1211
+ async signInWithHandle(handle) {
1212
+ log("signing in with handle:", handle);
1213
+ if (!this.config.projectId) {
1214
+ throw new Error("Project ID is required for authentication");
1215
+ }
1216
+ const resolved = await resolveHandle(handle);
1217
+ log("Resolved handle to PDS:", resolved.pdsUrl);
1218
+ const endpoints = {
1219
+ pds_url: resolved.pdsUrl,
1220
+ authorization_endpoint: resolved.authorization_endpoint,
1221
+ token_endpoint: resolved.token_endpoint,
1222
+ userinfo_endpoint: resolved.userinfo_endpoint
1223
+ };
1224
+ const signInLink = await this.getSignInUrl(void 0, endpoints);
1225
+ log("Generated federated sign-in link:", signInLink);
1226
+ try {
1227
+ new URL(signInLink);
1228
+ } catch {
1229
+ throw new Error("Failed to generate valid sign-in URL");
1230
+ }
1231
+ window.location.href = signInLink;
1232
+ }
1233
+ async signInWithCode(code, state) {
1234
+ try {
1235
+ log("signInWithCode called with code:", code);
1236
+ if (!code || typeof code !== "string") {
1237
+ return { success: false, error: "Invalid authorization code" };
1238
+ }
1239
+ if (state) {
1240
+ const storedState = await this.storage.get(STORAGE_KEYS.AUTH_STATE);
1241
+ if (storedState && storedState !== state) {
1242
+ log("State parameter mismatch:", { provided: state, stored: storedState });
1243
+ return { success: false, error: "State parameter mismatch" };
1244
+ }
1245
+ }
1246
+ await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
1247
+ cleanOAuthParamsFromUrl();
1248
+ this.freshSignIn = true;
1249
+ const token = await this.exchangeToken(code, false);
1250
+ if (token) {
1251
+ log("signInWithCode successful");
1252
+ return { success: true };
1253
+ } else {
1254
+ return { success: false, error: "Failed to exchange code for token" };
1255
+ }
1256
+ } catch (error) {
1257
+ log("signInWithCode error:", error);
1258
+ return {
1259
+ success: false,
1260
+ error: error.message || "Authentication failed"
1261
+ };
1262
+ }
1263
+ }
1264
+ /**
1265
+ * Clear auth state and storage. Does NOT handle sync/DB cleanup —
1266
+ * the UI layer (BasicProvider) wraps this to add sync teardown.
1267
+ */
1268
+ async signOut() {
1269
+ log("signing out!");
1270
+ this.resetAuthState();
1271
+ await this.storage.remove(STORAGE_KEYS.AUTH_STATE);
1272
+ await this.storage.remove(STORAGE_KEYS.LAST_CONNECT_REPORT);
1273
+ await this.clearStoredAuth();
1274
+ this.broadcastSignOut();
1275
+ this.notify();
1276
+ }
1277
+ hasScope(scope) {
1278
+ if (!this.tokenScope) return false;
1279
+ return this.tokenScope.split(/[\s,]+/).filter(Boolean).includes(scope);
1280
+ }
1281
+ /**
1282
+ * Returns scopes that were requested but not granted in the current token.
1283
+ * Useful after login or when a 403 is returned.
1284
+ */
1285
+ missingScopes() {
1286
+ const requested = this.requestedScopes.split(/[\s,]+/).filter(Boolean);
1287
+ if (!this.tokenScope) return requested;
1288
+ const granted = new Set(this.tokenScope.split(/[\s,]+/).filter(Boolean));
1289
+ return requested.filter((s) => !granted.has(s));
1290
+ }
1291
+ /**
1292
+ * Register online/offline handlers that retry pending refreshes.
1293
+ * Returns a cleanup function for useEffect teardown.
1294
+ */
1295
+ setupNetworkListeners() {
1296
+ const handleOnline = async () => {
1297
+ log("Network came back online");
1298
+ this.isOnline = true;
1299
+ if (this.pendingRefresh && this.token) {
1300
+ log("Retrying pending token refresh");
1301
+ this.pendingRefresh = false;
1302
+ const refreshToken = this.token.refresh_token || await this.storage.get(STORAGE_KEYS.REFRESH_TOKEN);
1303
+ if (refreshToken) {
1304
+ this.exchangeToken(refreshToken, true).catch((error) => {
1305
+ log("Retry refresh failed:", error);
1306
+ });
1307
+ }
1308
+ }
1309
+ };
1310
+ const handleOffline = () => {
1311
+ log("Network went offline");
1312
+ this.isOnline = false;
1313
+ };
1314
+ window.addEventListener("online", handleOnline);
1315
+ window.addEventListener("offline", handleOffline);
1316
+ return () => {
1317
+ window.removeEventListener("online", handleOnline);
1318
+ window.removeEventListener("offline", handleOffline);
1319
+ };
1320
+ }
1321
+ // ------------------------------------------------------------------
1322
+ // Private
1323
+ // ------------------------------------------------------------------
1324
+ get adminHostname() {
1325
+ try {
1326
+ return new URL(this.config.adminUrl).hostname;
1327
+ } catch {
1328
+ return "api.basic.tech";
1329
+ }
1330
+ }
1331
+ defaultPdsEndpoints() {
1332
+ return {
1333
+ pds_url: this.config.pdsUrl,
1334
+ authorization_endpoint: `${this.config.pdsUrl}/auth/authorize`,
1335
+ token_endpoint: `${this.config.pdsUrl}/auth/token`,
1336
+ userinfo_endpoint: `${this.config.pdsUrl}/auth/userinfo`
1337
+ };
1338
+ }
1339
+ async getActivePdsEndpoints() {
1340
+ const stored = await this.storage.get(STORAGE_KEYS.PDS_ENDPOINTS);
1341
+ if (stored) {
1342
+ try {
1343
+ return JSON.parse(stored);
1344
+ } catch {
1345
+ }
1346
+ }
1347
+ return this.defaultPdsEndpoints();
1348
+ }
1349
+ async reportConnection(accessToken) {
1350
+ if (!this.config.projectId || !this.config.adminUrl) return;
1351
+ const lastReport = await this.storage.get(STORAGE_KEYS.LAST_CONNECT_REPORT);
1352
+ if (lastReport) {
1353
+ const elapsed = Date.now() - parseInt(lastReport, 10);
1354
+ if (elapsed < 24 * 60 * 60 * 1e3) return;
1355
+ }
1356
+ try {
1357
+ await fetch(`${this.config.adminUrl}/project/${this.config.projectId}/user/connect`, {
1358
+ method: "POST",
1359
+ headers: { "Content-Type": "application/json" },
1360
+ body: JSON.stringify({ token: accessToken })
1361
+ });
1362
+ await this.storage.set(STORAGE_KEYS.LAST_CONNECT_REPORT, Date.now().toString());
1363
+ log("Reported connection to admin server");
1364
+ } catch (err) {
1365
+ log("Failed to report connection (non-blocking):", err);
1366
+ }
1367
+ }
1368
+ /**
1369
+ * After a new token is stored, decode JWT claims and fetch user info.
1370
+ */
1371
+ async processNewToken() {
1372
+ if (!this.token) {
1373
+ this.isAuthReady = true;
1374
+ this.notify();
1375
+ return;
1376
+ }
1377
+ try {
1378
+ const decoded = (0, import_jwt_decode.jwtDecode)(this.token.access_token);
1379
+ if (decoded.sub) this.did = decoded.sub;
1380
+ if (decoded.scope) this.tokenScope = decoded.scope;
1381
+ const expirationBuffer = 5;
1382
+ const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
1383
+ if (isExpired) {
1384
+ log("token is expired - refreshing ...");
1385
+ const refreshToken = this.token.refresh_token;
1386
+ if (!refreshToken) {
1387
+ log("Error: No refresh token available for expired token");
1388
+ this.isAuthReady = true;
1389
+ this.notify();
1390
+ return;
1391
+ }
1392
+ try {
1393
+ const newToken = await this.exchangeToken(refreshToken, true);
1394
+ await this.fetchUser(newToken?.access_token || "");
1395
+ } catch (error) {
1396
+ log("Failed to refresh token in processNewToken:", error);
1397
+ if (this.isNetworkError(error)) {
1398
+ log("Network issue - continuing with expired token until online");
1399
+ await this.fetchUser(this.token.access_token);
1400
+ } else {
1401
+ this.isAuthReady = true;
1402
+ this.notify();
1403
+ }
1404
+ }
1405
+ } else {
1406
+ await this.fetchUser(this.token.access_token);
1407
+ }
1408
+ } catch (error) {
1409
+ log("Error processing token:", error);
1410
+ this.isAuthReady = true;
1411
+ this.notify();
1412
+ }
1413
+ }
1414
+ async fetchUser(accessToken) {
1415
+ log("fetching user");
1416
+ try {
1417
+ const endpoints = await this.getActivePdsEndpoints();
1418
+ const response = await fetch(endpoints.userinfo_endpoint, {
1419
+ method: "GET",
1420
+ headers: { "Authorization": `Bearer ${accessToken}` }
1421
+ });
1422
+ if (!response.ok) {
1423
+ throw new Error(`Failed to fetch user info: ${response.status}`);
1424
+ }
1425
+ const user = await response.json();
1426
+ if (user.error) {
1427
+ log("error fetching user", user.error);
1428
+ throw new Error(`User info error: ${user.error}`);
1429
+ }
1430
+ if (this.token?.refresh_token) {
1431
+ await this.storage.set(STORAGE_KEYS.REFRESH_TOKEN, this.token.refresh_token);
1432
+ }
1433
+ await this.storage.set(STORAGE_KEYS.USER_INFO, JSON.stringify(user));
1434
+ log("Cached user info in storage");
1435
+ this.user = user;
1436
+ this.isSignedIn = true;
1437
+ this.isAuthReady = true;
1438
+ if (this.freshSignIn) {
1439
+ this.freshSignIn = false;
1440
+ this.broadcastSignIn();
1441
+ } else {
1442
+ this.broadcastTokenRefresh();
1443
+ }
1444
+ this.notify();
1445
+ } catch (error) {
1446
+ log("Failed to fetch user info:", error);
1447
+ this.isAuthReady = true;
1448
+ this.notify();
1449
+ }
1450
+ }
1451
+ /**
1452
+ * Exchange an auth code or refresh token for an access token.
1453
+ * Handles mutex (one in-flight refresh), token validation, and
1454
+ * triggers processNewToken on success.
1455
+ */
1456
+ async exchangeToken(codeOrRefreshToken, isRefreshToken) {
1457
+ if (!codeOrRefreshToken || codeOrRefreshToken.trim() === "") {
1458
+ const errorMsg = isRefreshToken ? "Refresh token is empty or undefined" : "Authorization code is empty or undefined";
1459
+ log("Error:", errorMsg);
1460
+ throw new Error(errorMsg);
1461
+ }
1462
+ if (isRefreshToken && this.refreshPromise) {
1463
+ log("Reusing in-flight refresh token request");
1464
+ return this.refreshPromise;
1465
+ }
1466
+ if (!isRefreshToken && this.codeExchangePromise) {
1467
+ log("Reusing in-flight code exchange request");
1468
+ return this.codeExchangePromise;
1469
+ }
1470
+ const tokenPromise = (async () => {
1471
+ try {
1472
+ if (!this.isOnline) {
1473
+ log("Network is offline, marking refresh as pending");
1474
+ this.pendingRefresh = true;
1475
+ throw new Error("Network offline - refresh will be retried when online");
1476
+ }
1477
+ const endpoints = await this.getActivePdsEndpoints();
1478
+ let requestBody;
1479
+ if (isRefreshToken) {
1480
+ requestBody = {
1481
+ grant_type: "refresh_token",
1482
+ refresh_token: codeOrRefreshToken
1483
+ };
1484
+ if (this.config.projectId) {
1485
+ requestBody.client_id = normalizeClientId(this.config.projectId, this.adminHostname);
1486
+ }
1487
+ } else {
1488
+ requestBody = {
1489
+ grant_type: "authorization_code",
1490
+ code: codeOrRefreshToken
1491
+ };
1492
+ const storedRedirectUri = await this.storage.get(STORAGE_KEYS.REDIRECT_URI);
1493
+ if (storedRedirectUri) {
1494
+ requestBody.redirect_uri = storedRedirectUri;
1495
+ log("Including redirect_uri in token exchange:", storedRedirectUri);
1496
+ } else {
1497
+ log("Warning: No redirect_uri found in storage for token exchange");
1498
+ }
1499
+ const codeVerifier = await this.storage.get(STORAGE_KEYS.CODE_VERIFIER);
1500
+ if (codeVerifier) {
1501
+ requestBody.code_verifier = codeVerifier;
1502
+ }
1503
+ if (this.config.projectId) {
1504
+ requestBody.client_id = normalizeClientId(this.config.projectId, this.adminHostname);
1505
+ }
1506
+ }
1507
+ log("Token exchange request body:", {
1508
+ ...requestBody,
1509
+ ...isRefreshToken ? { refresh_token: "[REDACTED]" } : { code: "[REDACTED]" },
1510
+ ...requestBody.code_verifier ? { code_verifier: "[REDACTED]" } : {}
1511
+ });
1512
+ const token = await fetch(endpoints.token_endpoint, {
1513
+ method: "POST",
1514
+ headers: { "Content-Type": "application/json" },
1515
+ body: JSON.stringify(requestBody)
1516
+ }).then((response) => response.json()).catch((error) => {
1517
+ log("Network error fetching token:", error);
1518
+ if (!this.isOnline) {
1519
+ this.pendingRefresh = true;
1520
+ throw new Error("Network offline - refresh will be retried when online");
1521
+ }
1522
+ throw new Error("Network error during token refresh");
1523
+ });
1524
+ if (token.access_token) {
1525
+ try {
1526
+ const decoded = (0, import_jwt_decode.jwtDecode)(token.access_token);
1527
+ if (decoded.typ === "refresh") {
1528
+ log("Error: received refresh token as access token");
1529
+ throw new Error("Invalid token: received refresh token instead of access token");
1530
+ }
1531
+ } catch (decodeError) {
1532
+ if (decodeError.message.includes("Invalid token")) {
1533
+ throw decodeError;
1534
+ }
1535
+ log("Warning: could not decode access token for type check:", decodeError);
1536
+ }
1537
+ }
1538
+ if (token.error) {
1539
+ log("error fetching token", token.error);
1540
+ if (typeof token.error === "string" && (token.error.includes("network") || token.error.includes("timeout"))) {
1541
+ this.pendingRefresh = true;
1542
+ throw new Error("Network issue - refresh will be retried when online");
1543
+ }
1544
+ await this.clearStoredAuth();
1545
+ this.resetAuthState();
1546
+ this.notify();
1547
+ throw new Error(`Token refresh failed: ${token.error}`);
1548
+ } else {
1549
+ this.token = token;
1550
+ this.pendingRefresh = false;
1551
+ if (token.refresh_token) {
1552
+ await this.storage.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token);
1553
+ log("Updated refresh token in storage");
1554
+ }
1555
+ if (!isRefreshToken) {
1556
+ await this.storage.remove(STORAGE_KEYS.REDIRECT_URI);
1557
+ await this.storage.remove(STORAGE_KEYS.CODE_VERIFIER);
1558
+ log("Cleaned up redirect_uri and code_verifier from storage after successful exchange");
1559
+ }
1560
+ this.reportConnection(token.access_token).catch(() => {
1561
+ });
1562
+ await this.processNewToken();
1563
+ }
1564
+ return token;
1565
+ } catch (error) {
1566
+ log("Token refresh error:", error);
1567
+ if (!this.isNetworkError(error)) {
1568
+ await this.clearStoredAuth();
1569
+ this.resetAuthState();
1570
+ this.notify();
1571
+ }
1572
+ throw error;
1573
+ }
1574
+ })();
1575
+ if (isRefreshToken) {
1576
+ this.refreshPromise = tokenPromise;
1577
+ tokenPromise.finally(() => {
1578
+ if (this.refreshPromise === tokenPromise) {
1579
+ this.refreshPromise = null;
1580
+ log("Cleared refresh promise reference");
1581
+ }
1582
+ });
1583
+ } else {
1584
+ this.codeExchangePromise = tokenPromise;
1585
+ tokenPromise.finally(() => {
1586
+ if (this.codeExchangePromise === tokenPromise) {
1587
+ this.codeExchangePromise = null;
1588
+ log("Cleared code exchange promise reference");
1589
+ }
1590
+ });
1591
+ }
1592
+ return tokenPromise;
1593
+ }
1594
+ resetAuthState() {
1595
+ this.user = null;
1596
+ this.isSignedIn = false;
1597
+ this.token = null;
1598
+ this.did = null;
1599
+ this.tokenScope = null;
1600
+ this.isAuthReady = true;
1601
+ }
1602
+ async clearStoredAuth() {
1603
+ await this.storage.remove(STORAGE_KEYS.REFRESH_TOKEN);
1604
+ await this.storage.remove(STORAGE_KEYS.USER_INFO);
1605
+ await this.storage.remove(STORAGE_KEYS.REDIRECT_URI);
1606
+ await this.storage.remove(STORAGE_KEYS.CODE_VERIFIER);
1607
+ await this.storage.remove(STORAGE_KEYS.SERVER_URL);
1608
+ await this.storage.remove(STORAGE_KEYS.PDS_ENDPOINTS);
1609
+ }
1610
+ isNetworkError(error) {
1611
+ if (error instanceof Error) {
1612
+ return error.message.includes("offline") || error.message.includes("Network");
1613
+ }
1614
+ return false;
1615
+ }
1616
+ };
1617
+
1618
+ // src/AuthContext.tsx
1619
+ init_config();
1620
+
1621
+ // src/updater/versionUpdater.ts
1622
+ init_config();
1623
+ var VersionUpdater = class {
1624
+ storage;
1625
+ currentVersion;
1626
+ migrations;
1627
+ versionKey = "basic_app_version";
1628
+ constructor(storage, currentVersion, migrations = []) {
1629
+ this.storage = storage;
1630
+ this.currentVersion = currentVersion;
1631
+ this.migrations = migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
1632
+ }
1633
+ /**
1634
+ * Check current stored version and run migrations if needed
1635
+ * Only compares major.minor versions, ignoring beta/prerelease parts
1636
+ * Example: "0.7.0-beta.1" and "0.7.0" are treated as the same version
1637
+ */
1638
+ async checkAndUpdate() {
1639
+ const storedVersion = await this.getStoredVersion();
1640
+ if (!storedVersion) {
1641
+ await this.setStoredVersion(this.currentVersion);
1642
+ return { updated: false, toVersion: this.currentVersion };
1643
+ }
1644
+ if (storedVersion === this.currentVersion) {
1645
+ return { updated: false, toVersion: this.currentVersion };
1646
+ }
1647
+ const migrationsToRun = this.getMigrationsToRun(storedVersion, this.currentVersion);
1648
+ if (migrationsToRun.length === 0) {
1649
+ await this.setStoredVersion(this.currentVersion);
1650
+ return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion };
1651
+ }
1652
+ for (const migration of migrationsToRun) {
1653
+ try {
1654
+ log(`Running migration from ${migration.fromVersion} to ${migration.toVersion}`);
1655
+ await migration.migrate(this.storage);
1656
+ } catch (error) {
1657
+ console.error(`Migration failed from ${migration.fromVersion} to ${migration.toVersion}:`, error);
1658
+ throw new Error(`Migration failed: ${error}`);
1659
+ }
1660
+ }
1661
+ await this.setStoredVersion(this.currentVersion);
1662
+ return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion };
1663
+ }
1664
+ async getStoredVersion() {
1665
+ try {
1666
+ const versionData = await this.storage.get(this.versionKey);
1667
+ if (!versionData) return null;
1668
+ const versionInfo = JSON.parse(versionData);
1669
+ return versionInfo.version;
1670
+ } catch (error) {
1671
+ console.warn("Failed to get stored version:", error);
1672
+ return null;
1673
+ }
1674
+ }
1675
+ async setStoredVersion(version2) {
1676
+ const versionInfo = {
1677
+ version: version2,
1678
+ lastUpdated: Date.now()
1679
+ };
1680
+ await this.storage.set(this.versionKey, JSON.stringify(versionInfo));
1681
+ }
1682
+ getMigrationsToRun(fromVersion, toVersion) {
1683
+ return this.migrations.filter((migration) => {
1684
+ const storedLessThanMigrationTo = this.compareVersions(fromVersion, migration.toVersion) < 0;
1685
+ const currentGreaterThanOrEqualMigrationTo = this.compareVersions(toVersion, migration.toVersion) >= 0;
1686
+ const shouldRun = storedLessThanMigrationTo && currentGreaterThanOrEqualMigrationTo;
1687
+ log(`Migration ${migration.fromVersion} \u2192 ${migration.toVersion}: shouldRun=${shouldRun}`);
1688
+ return shouldRun;
1689
+ });
1690
+ }
1691
+ /**
1692
+ * Simple semantic version comparison (major.minor only, ignoring beta/prerelease)
1693
+ * Returns: -1 if a < b, 0 if a === b, 1 if a > b
1694
+ */
1695
+ compareVersions(a, b) {
1696
+ const aMajorMinor = this.extractMajorMinor(a);
1697
+ const bMajorMinor = this.extractMajorMinor(b);
1698
+ if (aMajorMinor.major !== bMajorMinor.major) {
1699
+ return aMajorMinor.major - bMajorMinor.major;
1700
+ }
1701
+ return aMajorMinor.minor - bMajorMinor.minor;
1702
+ }
1703
+ /**
1704
+ * Extract major.minor from version string, ignoring beta/prerelease
1705
+ * Examples: "0.7.0-beta.1" -> {major: 0, minor: 7}
1706
+ * "1.2.3" -> {major: 1, minor: 2}
1707
+ */
1708
+ extractMajorMinor(version2) {
1709
+ const cleanVersion = version2.split("-")[0]?.split("+")[0] || version2;
1710
+ const parts = cleanVersion.split(".").map(Number);
1711
+ return {
1712
+ major: parts[0] || 0,
1713
+ minor: parts[1] || 0
1714
+ };
1715
+ }
1716
+ /**
1717
+ * Add a migration to the updater
1718
+ */
1719
+ addMigration(migration) {
1720
+ this.migrations.push(migration);
1721
+ this.migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion));
1722
+ }
1723
+ };
1724
+ function createVersionUpdater(storage, currentVersion, migrations = []) {
1725
+ return new VersionUpdater(storage, currentVersion, migrations);
1726
+ }
1727
+
1728
+ // src/updater/updateMigrations.ts
1729
+ init_config();
1730
+ var addMigrationTimestamp = {
1731
+ fromVersion: "0.6.0",
1732
+ toVersion: "0.7.0",
1733
+ async migrate(storage) {
1734
+ log("Running migration 0.6.0 \u2192 0.7.0");
1735
+ storage.set("test_migration", "true");
1736
+ }
1737
+ };
1738
+ function getMigrations() {
1739
+ return [
1740
+ addMigrationTimestamp
1741
+ ];
1742
+ }
1743
+
1744
+ // src/utils/schema.ts
1745
+ var import_schema3 = require("@basictech/schema");
1746
+ init_config();
1747
+ async function getSchemaStatus(schema) {
1748
+ const projectId = schema.project_id;
1749
+ const valid = (0, import_schema3.validateSchema)(schema);
1750
+ if (!valid.valid) {
1751
+ console.warn("BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled");
1752
+ return {
1753
+ valid: false,
1754
+ status: "invalid",
1755
+ latest: null
1756
+ };
1757
+ }
1758
+ const latestSchema = await fetch(`https://api.basic.tech/project/${projectId}/schema`).then((res) => res.json()).then((data) => data.data[0].schema).catch((err) => {
1759
+ return {
1760
+ valid: false,
1761
+ status: "error",
1762
+ latest: null
1763
+ };
1764
+ });
1765
+ if (!latestSchema.version) {
1766
+ return {
1767
+ valid: false,
1768
+ status: "error",
1769
+ latest: null
1770
+ };
1771
+ }
1772
+ if (latestSchema.version > schema.version) {
1773
+ console.warn("BasicDB Error: your local schema version is behind the latest. Found version:", schema.version, "but expected", latestSchema.version, " - sync is disabled");
1774
+ return {
1775
+ valid: false,
1776
+ status: "behind",
1777
+ latest: latestSchema
1778
+ };
1779
+ } else if (latestSchema.version < schema.version) {
1780
+ console.warn("BasicDB Error: your local schema version is ahead of the latest. Found version:", schema.version, "but expected", latestSchema.version, " - sync is disabled");
1781
+ return {
1782
+ valid: false,
1783
+ status: "ahead",
1784
+ latest: latestSchema
1785
+ };
1786
+ } else if (latestSchema.version === schema.version) {
1787
+ const changes = (0, import_schema3.compareSchemas)(schema, latestSchema);
1788
+ if (changes.valid) {
1789
+ return {
1790
+ valid: true,
1791
+ status: "current",
1792
+ latest: latestSchema
1793
+ };
1794
+ } else {
1795
+ 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");
1796
+ return {
1797
+ valid: false,
1798
+ status: "conflict",
1799
+ latest: latestSchema
1800
+ };
1801
+ }
1802
+ } else {
1803
+ return {
1804
+ valid: false,
1805
+ status: "error",
1806
+ latest: null
1807
+ };
1808
+ }
1809
+ }
1810
+ async function validateAndCheckSchema(schema) {
1811
+ const valid = (0, import_schema3.validateSchema)(schema);
1812
+ if (!valid.valid) {
1813
+ log("Basic Schema is invalid!", valid.errors);
1814
+ console.group("Schema Errors");
1815
+ let errorMessage = "";
1816
+ valid.errors.forEach((error, index) => {
1817
+ log(`${index + 1}:`, error.message, ` - at ${error.instancePath}`);
980
1818
  errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}
981
1819
  `;
982
1820
  });
@@ -992,6 +1830,7 @@ async function validateAndCheckSchema(schema) {
992
1830
  schemaStatus = await getSchemaStatus(schema);
993
1831
  log("schemaStatus", schemaStatus);
994
1832
  } else {
1833
+ schemaStatus = { valid: false, status: "unpublished" };
995
1834
  log("schema not published - at version 0");
996
1835
  }
997
1836
  return {
@@ -1004,9 +1843,21 @@ async function validateAndCheckSchema(schema) {
1004
1843
  var import_jsx_runtime = require("react/jsx-runtime");
1005
1844
  var DEFAULT_AUTH_CONFIG = {
1006
1845
  scopes: "profile,email,app:admin",
1007
- server_url: "https://api.basic.tech",
1846
+ pds_url: "https://pds.basic.id",
1847
+ admin_url: "https://api.basic.tech",
1008
1848
  ws_url: "wss://pds.basic.id/ws"
1009
1849
  };
1850
+ var DBStatus = /* @__PURE__ */ ((DBStatus2) => {
1851
+ DBStatus2["LOADING"] = "LOADING";
1852
+ DBStatus2["OFFLINE"] = "OFFLINE";
1853
+ DBStatus2["CONNECTING"] = "CONNECTING";
1854
+ DBStatus2["ONLINE"] = "ONLINE";
1855
+ DBStatus2["SYNCING"] = "SYNCING";
1856
+ DBStatus2["ERROR"] = "ERROR";
1857
+ DBStatus2["ERROR_WILL_RETRY"] = "ERROR_WILL_RETRY";
1858
+ DBStatus2["ERROR_TOKEN_EXPIRED"] = "ERROR_TOKEN_EXPIRED";
1859
+ return DBStatus2;
1860
+ })(DBStatus || {});
1010
1861
  var noDb = {
1011
1862
  collection: () => {
1012
1863
  throw new Error("no basicdb found - initialization failed. double check your schema.");
@@ -1017,12 +1868,17 @@ var BasicContext = (0, import_react.createContext)({
1017
1868
  isReady: false,
1018
1869
  isSignedIn: false,
1019
1870
  user: null,
1871
+ did: null,
1872
+ scope: null,
1873
+ hasScope: () => false,
1874
+ missingScopes: () => [],
1020
1875
  // Auth actions
1021
1876
  signIn: () => Promise.resolve(),
1877
+ signInWithHandle: () => Promise.resolve(),
1022
1878
  signOut: () => Promise.resolve(),
1023
1879
  signInWithCode: () => Promise.resolve({ success: false }),
1024
1880
  // Token management
1025
- getToken: () => Promise.reject(new Error("no token")),
1881
+ getToken: (_options) => Promise.reject(new Error("no token")),
1026
1882
  getSignInUrl: () => Promise.resolve(""),
1027
1883
  // DB access
1028
1884
  db: noDb,
@@ -1035,6 +1891,16 @@ var BasicContext = (0, import_react.createContext)({
1035
1891
  signinWithCode: () => Promise.resolve({ success: false }),
1036
1892
  getSignInLink: () => Promise.resolve("")
1037
1893
  });
1894
+ function snapshotAuth(mgr) {
1895
+ return {
1896
+ isSignedIn: mgr.isSignedIn,
1897
+ hasToken: !!mgr.token,
1898
+ isAuthReady: mgr.isAuthReady,
1899
+ user: mgr.user,
1900
+ did: mgr.did,
1901
+ tokenScope: mgr.tokenScope
1902
+ };
1903
+ }
1038
1904
  function BasicProvider({
1039
1905
  children,
1040
1906
  project_id: project_id_prop,
@@ -1045,64 +1911,79 @@ function BasicProvider({
1045
1911
  dbMode = "sync"
1046
1912
  }) {
1047
1913
  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();
1914
+ if (auth?.server_url && !auth?.pds_url) {
1915
+ log("Warning: auth.server_url is deprecated, use auth.pds_url instead");
1916
+ }
1061
1917
  const authConfig = {
1062
1918
  scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
1063
- server_url: auth?.server_url || DEFAULT_AUTH_CONFIG.server_url,
1919
+ pds_url: auth?.pds_url || auth?.server_url || DEFAULT_AUTH_CONFIG.pds_url,
1920
+ admin_url: auth?.admin_url || DEFAULT_AUTH_CONFIG.admin_url,
1064
1921
  ws_url: auth?.ws_url || DEFAULT_AUTH_CONFIG.ws_url
1065
1922
  };
1066
1923
  const scopesString = Array.isArray(authConfig.scopes) ? authConfig.scopes.join(" ") : authConfig.scopes;
1067
- const refreshPromiseRef = (0, import_react.useRef)(null);
1924
+ const storageRef = (0, import_react.useRef)(storage || new LocalStorageAdapter());
1925
+ const storageAdapter = storageRef.current;
1926
+ const [authState, setAuthState] = (0, import_react.useState)({
1927
+ isSignedIn: false,
1928
+ hasToken: false,
1929
+ isAuthReady: false,
1930
+ user: null,
1931
+ did: null,
1932
+ tokenScope: null
1933
+ });
1934
+ const authRef = (0, import_react.useRef)(null);
1935
+ if (!authRef.current) {
1936
+ authRef.current = new AuthManager(
1937
+ {
1938
+ projectId: project_id,
1939
+ scopes: scopesString,
1940
+ pdsUrl: authConfig.pds_url,
1941
+ adminUrl: authConfig.admin_url,
1942
+ debug
1943
+ },
1944
+ storageAdapter,
1945
+ () => setAuthState(snapshotAuth(authRef.current))
1946
+ );
1947
+ }
1948
+ const syncRef = (0, import_react.useRef)(null);
1949
+ const remoteDbRef = (0, import_react.useRef)(null);
1950
+ const [shouldConnect, setShouldConnect] = (0, import_react.useState)(false);
1951
+ const [dbStatus, setDbStatus] = (0, import_react.useState)("OFFLINE" /* OFFLINE */);
1952
+ const [isReady, setIsReady] = (0, import_react.useState)(false);
1953
+ const [error, setError] = (0, import_react.useState)(null);
1068
1954
  const isDevMode = () => isDevelopment(debug);
1069
- const cleanOAuthParams = () => cleanOAuthParamsFromUrl();
1070
1955
  (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
- }
1956
+ const runVersionUpdater = async () => {
1957
+ try {
1958
+ const versionUpdater = createVersionUpdater(storageAdapter, version, getMigrations());
1959
+ const updateResult = await versionUpdater.checkAndUpdate();
1960
+ if (updateResult.updated) {
1961
+ log(`App updated from ${updateResult.fromVersion} to ${updateResult.toVersion}`);
1962
+ } else {
1963
+ log(`App version ${updateResult.toVersion} is current`);
1084
1964
  }
1965
+ } catch (error2) {
1966
+ log("Version update failed:", error2);
1085
1967
  }
1086
1968
  };
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]);
1969
+ runVersionUpdater();
1970
+ authRef.current.initialize();
1971
+ return authRef.current.setupNetworkListeners();
1972
+ }, []);
1098
1973
  (0, import_react.useEffect)(() => {
1099
1974
  async function initSyncDb(options) {
1100
1975
  if (!syncRef.current) {
1101
1976
  log("Initializing Basic Sync DB");
1102
1977
  await initDexieExtensions();
1103
1978
  syncRef.current = new BasicSync("basicdb", { schema });
1104
- syncRef.current.syncable.on("statusChanged", (status, url) => {
1105
- setDbStatus(getSyncStatus(status));
1979
+ syncRef.current.syncable.on("statusChanged", (status) => {
1980
+ const newStatus = getSyncStatus(status);
1981
+ setDbStatus(newStatus);
1982
+ if (newStatus === "ERROR_WILL_RETRY" /* ERROR_WILL_RETRY */) {
1983
+ log("Sync entered ERROR_WILL_RETRY - proactively refreshing token");
1984
+ authRef.current.getToken({ forceRefresh: true }).catch(() => {
1985
+ });
1986
+ }
1106
1987
  });
1107
1988
  if (options.shouldConnect) {
1108
1989
  setShouldConnect(true);
@@ -1125,14 +2006,14 @@ function BasicProvider({
1125
2006
  }
1126
2007
  log("Initializing Basic Remote DB");
1127
2008
  remoteDbRef.current = new RemoteDB({
1128
- serverUrl: authConfig.server_url,
2009
+ serverUrl: authConfig.pds_url,
1129
2010
  projectId: project_id,
1130
- getToken,
2011
+ getToken: (opts) => authRef.current.getToken(opts),
1131
2012
  schema,
1132
2013
  debug,
1133
2014
  onAuthError: (error2) => {
1134
2015
  log("RemoteDB auth error:", error2);
1135
- signout();
2016
+ handleSignOut();
1136
2017
  }
1137
2018
  });
1138
2019
  setDbStatus("ONLINE" /* ONLINE */);
@@ -1163,7 +2044,11 @@ function BasicProvider({
1163
2044
  if (result.schemaStatus.valid) {
1164
2045
  await initSyncDb({ shouldConnect: true });
1165
2046
  } else {
1166
- log("Schema is invalid!", result.schemaStatus);
2047
+ if (result.schemaStatus.status === "unpublished") {
2048
+ log("Schema not published yet (version 0) - sync is disabled. Publish your schema to enable sync.");
2049
+ } else {
2050
+ log("Schema is invalid!", result.schemaStatus);
2051
+ }
1167
2052
  await initSyncDb({ shouldConnect: false });
1168
2053
  }
1169
2054
  }
@@ -1180,220 +2065,33 @@ function BasicProvider({
1180
2065
  }
1181
2066
  }, []);
1182
2067
  (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
- }
2068
+ if (authState.hasToken && syncRef.current && authState.isSignedIn && shouldConnect) {
2069
+ log("connecting to db...");
2070
+ syncRef.current?.connect({
2071
+ getToken: (opts) => authRef.current.getToken(opts),
2072
+ ws_url: authConfig.ws_url
2073
+ }).catch((e) => {
2074
+ log("error connecting to db", e);
2075
+ });
1198
2076
  }
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");
2077
+ }, [authState.isSignedIn, authState.hasToken, shouldConnect]);
2078
+ const handleSignOut = async () => {
2079
+ await authRef.current.signOut();
2080
+ if (syncRef.current) {
1285
2081
  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);
2082
+ await syncRef.current.close();
2083
+ await syncRef.current.delete({ disableAutoOpen: false });
2084
+ syncRef.current = null;
2085
+ window?.location?.reload();
1310
2086
  } 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;
1320
- }
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 || "");
2087
+ console.error("Error during database cleanup:", error2);
1346
2088
  }
1347
2089
  }
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
- }
1378
2090
  };
1379
- const signin = async () => {
2091
+ const handleSignIn = async () => {
1380
2092
  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;
2093
+ await authRef.current.signIn();
1395
2094
  } catch (error2) {
1396
- log("Error during sign-in:", error2);
1397
2095
  if (isDevMode()) {
1398
2096
  setError({
1399
2097
  code: "signin_error",
@@ -1404,255 +2102,19 @@ function BasicProvider({
1404
2102
  throw error2;
1405
2103
  }
1406
2104
  };
1407
- const signinWithCode = async (code, state) => {
2105
+ const handleSignInWithHandle = async (handle) => {
1408
2106
  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
- }
2107
+ await authRef.current.signInWithHandle(handle);
1429
2108
  } 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");
2109
+ if (isDevMode()) {
2110
+ setError({
2111
+ code: "signin_error",
2112
+ title: "Sign-in Failed",
2113
+ message: error2.message || "An error occurred during sign-in. Please try again."
1595
2114
  });
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
2115
  }
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
- });
2116
+ throw error2;
1654
2117
  }
1655
- return refreshPromise;
1656
2118
  };
1657
2119
  const getCurrentDb = () => {
1658
2120
  if (dbMode === "remote") {
@@ -1661,27 +2123,32 @@ function BasicProvider({
1661
2123
  return syncRef.current || noDb;
1662
2124
  };
1663
2125
  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,
2126
+ // Auth state
2127
+ isReady: authState.isAuthReady,
2128
+ isSignedIn: authState.isSignedIn,
2129
+ user: authState.user,
2130
+ did: authState.did,
2131
+ scope: authState.tokenScope,
2132
+ hasScope: (scope) => authRef.current.hasScope(scope),
2133
+ missingScopes: () => authRef.current.missingScopes(),
2134
+ // Auth actions
2135
+ signIn: handleSignIn,
2136
+ signInWithHandle: handleSignInWithHandle,
2137
+ signOut: handleSignOut,
2138
+ signInWithCode: (code, state) => authRef.current.signInWithCode(code, state),
1672
2139
  // Token management
1673
- getToken,
1674
- getSignInUrl: getSignInLink,
2140
+ getToken: (opts) => authRef.current.getToken(opts),
2141
+ getSignInUrl: (redirectUri) => authRef.current.getSignInUrl(redirectUri),
1675
2142
  // DB access
1676
2143
  db: getCurrentDb(),
1677
2144
  dbStatus,
1678
2145
  dbMode,
1679
2146
  // Legacy aliases (deprecated)
1680
- isAuthReady,
1681
- signin,
1682
- signout,
1683
- signinWithCode,
1684
- getSignInLink
2147
+ isAuthReady: authState.isAuthReady,
2148
+ signin: handleSignIn,
2149
+ signout: handleSignOut,
2150
+ signinWithCode: (code, state) => authRef.current.signInWithCode(code, state),
2151
+ getSignInLink: (redirectUri) => authRef.current.getSignInUrl(redirectUri)
1685
2152
  };
1686
2153
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(BasicContext.Provider, { value: contextValue, children: [
1687
2154
  error && isDevMode() && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorDisplay, { error }),
@@ -1720,11 +2187,15 @@ var import_dexie_react_hooks = require("dexie-react-hooks");
1720
2187
  // Annotate the CommonJS export names for ESM import in node:
1721
2188
  0 && (module.exports = {
1722
2189
  BasicProvider,
2190
+ DBStatus,
1723
2191
  NotAuthenticatedError,
1724
2192
  RemoteCollection,
1725
2193
  RemoteDB,
1726
2194
  RemoteDBError,
1727
2195
  STORAGE_KEYS,
2196
+ resolveDid,
2197
+ resolveDidWebUrl,
2198
+ resolveHandle,
1728
2199
  useBasic,
1729
2200
  useQuery
1730
2201
  });