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