@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/.turbo/turbo-build.log +13 -12
- package/AUTH_IMPLEMENTATION_GUIDE.md +20 -18
- package/changelog.md +18 -2
- package/dist/index.d.mts +78 -21
- package/dist/index.d.ts +78 -21
- package/dist/index.js +1250 -779
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1246 -779
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/readme.md +17 -1
- package/src/AuthContext.tsx +202 -702
- package/src/config.ts +1 -19
- package/src/core/auth/AuthManager.ts +858 -0
- package/src/core/db/RemoteCollection.ts +30 -16
- package/src/core/db/index.ts +1 -1
- package/src/core/db/types.ts +13 -1
- package/src/index.ts +7 -1
- package/src/sync/index.ts +15 -29
- package/src/sync/syncProtocol.js +84 -22
- package/src/sync/tokenRegistry.ts +20 -0
- package/src/updater/updateMigrations.ts +3 -3
- package/src/updater/versionUpdater.ts +3 -10
- package/src/utils/normalizeClientId.ts +22 -0
- package/src/utils/resolveDid.ts +101 -0
- package/src/utils/schema.ts +3 -4
- package/src/utils/storage.ts +4 -1
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
|
170
|
-
__export(
|
|
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(
|
|
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
|
-
|
|
196
|
-
if (
|
|
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({
|
|
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, {
|
|
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(([
|
|
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,
|
|
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 (
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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/
|
|
658
|
-
|
|
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
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
798
|
+
const pathParts = parts.slice(1).map((p) => decodeURIComponent(p));
|
|
799
|
+
return `https://${hostname}/${pathParts.join("/")}/did.json`;
|
|
821
800
|
}
|
|
822
|
-
function
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
|
839
|
-
|
|
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/
|
|
906
|
-
var import_schema3 = require("@basictech/schema");
|
|
919
|
+
// src/core/auth/AuthManager.ts
|
|
907
920
|
init_config();
|
|
908
|
-
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
|
1105
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
-
|
|
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
|
|
2091
|
+
const handleSignIn = async () => {
|
|
1380
2092
|
try {
|
|
1381
|
-
|
|
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
|
|
2105
|
+
const handleSignInWithHandle = async (handle) => {
|
|
1408
2106
|
try {
|
|
1409
|
-
|
|
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
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
|
1665
|
-
isReady: isAuthReady,
|
|
1666
|
-
isSignedIn,
|
|
1667
|
-
user,
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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:
|
|
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
|
});
|