diff --git a/.changeset/mean-mayflies-learn.md b/.changeset/mean-mayflies-learn.md new file mode 100644 index 000000000..09473a94a --- /dev/null +++ b/.changeset/mean-mayflies-learn.md @@ -0,0 +1,5 @@ +--- +'@powersync/diagnostics-app': minor +--- + +Improved error messages for some token or endpoint issues diff --git a/tools/diagnostics-app/README.md b/tools/diagnostics-app/README.md index 2a7d92169..3c0088228 100644 --- a/tools/diagnostics-app/README.md +++ b/tools/diagnostics-app/README.md @@ -11,6 +11,14 @@ The app is currently available at [https://diagnostics-app.powersync.com/](https It can also be run as a local standalone web app, and is largely based on the [web SDK](/packages/web/). +## Running the app with Docker + +```sh +docker run --pull always -p 8082:80 journeyapps/powersync-diagnostics-app +``` + +The app will be available on http://localhost:8082. + ## Running the app locally In the root of the repository, run: diff --git a/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts b/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts index c9b7df017..525952143 100644 --- a/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts +++ b/tools/diagnostics-app/src/library/powersync/ConnectionManager.ts @@ -21,7 +21,7 @@ export const getParams = () => { const stringifiedParams = localStorage.getItem(PARAMS_STORE); const params = safeParse(stringifiedParams); return params; -} +}; export const schemaManager = new DynamicSchemaManager(); @@ -88,7 +88,7 @@ export async function connect() { if (!sync.syncStatus.connected) { // Disconnect but don't wait for it sync.disconnect(); - throw syncErrorTracker.lastSyncError ?? new Error('Failed to conncet'); + throw syncErrorTracker.lastSyncError ?? new Error('Failed to connect'); } else { syncErrorTracker.lastSyncError = null; } @@ -112,6 +112,7 @@ export async function disconnect() { export async function signOut() { connector.clearCredentials(); await db.disconnectAndClear(); + await schemaManager.clear(); } export const setParams = (p: object) => { diff --git a/tools/diagnostics-app/src/library/powersync/TokenConnector.ts b/tools/diagnostics-app/src/library/powersync/TokenConnector.ts index 59e82c18a..49055d0f6 100644 --- a/tools/diagnostics-app/src/library/powersync/TokenConnector.ts +++ b/tools/diagnostics-app/src/library/powersync/TokenConnector.ts @@ -22,6 +22,8 @@ export class TokenConnector implements PowerSyncBackendConnector { } async signIn(credentials: Credentials) { + validateSecureContext(credentials.endpoint); + checkJWT(credentials.token); try { localStorage.setItem('powersync_credentials', JSON.stringify(credentials)); await connect(); @@ -39,3 +41,37 @@ export class TokenConnector implements PowerSyncBackendConnector { localStorage.removeItem('powersync_credentials'); } } + +function validateSecureContext(url: string) { + if (!location.href.startsWith('https:')) { + return; + } + const parsedUrl = new URL(url); + const secure = + parsedUrl.protocol === 'https:' || + parsedUrl.hostname === 'localhost' || + parsedUrl.hostname === '127.0.0.1' || + parsedUrl.hostname === '::1'; + if (!secure) { + throw new Error(`Cannot connect to http endpoints from the hosted diagnostics app. +Run either the PowerSync endpoint on http://localhost, or the diagnostics app on http://localhost.`); + } +} + +function checkJWT(token: string) { + // Split the token into parts by "." + const parts = token.split('.'); + + // Check that it has exactly three parts (header, payload, signature) + if (parts.length !== 3) { + throw new Error(`Token must be a JWT: Expected 3 parts, got ${parts.length}`); + } + + // Check that each part is base64 or base64url encoded + const base64UrlRegex = /^[A-Za-z0-9-_]+$/; + + const isBase64 = parts.every((part) => base64UrlRegex.test(part)); + if (!isBase64) { + throw new Error(`Token must be a JWT: Not all parts are base64 encoded`); + } +}