Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mean-mayflies-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/diagnostics-app': minor
---

Improved error messages for some token or endpoint issues
8 changes: 8 additions & 0 deletions tools/diagnostics-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const getParams = () => {
const stringifiedParams = localStorage.getItem(PARAMS_STORE);
const params = safeParse(stringifiedParams);
return params;
}
};

export const schemaManager = new DynamicSchemaManager();

Expand Down Expand Up @@ -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;
}
Expand All @@ -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) => {
Expand Down
36 changes: 36 additions & 0 deletions tools/diagnostics-app/src/library/powersync/TokenConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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`);
}
}
Loading