diff --git a/README.md b/README.md index 1d544dd..b2e8fca 100644 --- a/README.md +++ b/README.md @@ -42,18 +42,22 @@ Restart Obsidian and enable **Plaud Sync** under **Settings → Community plugin ## Obtaining your token -Plaud has no official API or developer portal. The token is a JWT session cookie stored in your browser when you use the Plaud web app. +Plaud has no official API or developer portal. The token is the short-lived `Bearer` access token the web app sends on every API request. (Older builds of this plugin told you to read `localStorage.getItem("tokenstr")` — Plaud has since removed that key, and the `frillSsoToken` value you may find in `localStorage` is for the feedback widget, **not** the API. Grab the live request header instead.) 1. Open [web.plaud.ai](https://web.plaud.ai/) and log in -2. Open your browser's Developer Tools (`F12` or `Cmd+Opt+I`) -3. Go to the **Console** tab and run: - ```js - localStorage.getItem("tokenstr") - ``` -4. Copy the full string (starts with `bearer eyJ...`) -5. In Obsidian, open **Settings → Plaud Sync** and paste it into the **Plaud token** field - -The token is long-lived (~10 months) but will eventually expire. When it does, repeat these steps. +2. Open Developer Tools (`F12` or `Cmd+Opt+I`) and select the **Network** tab +3. Type `simple` in the Network filter box, then reload the page (`Cmd+R` / `Ctrl+R`) +4. Click the request named **`web`** (`…/file/simple/web`) → **Headers** → **Request Headers** +5. Copy the full value of the **`authorization`** header (starts with `Bearer eyJ...`) +6. In Obsidian, open **Settings → Plaud Sync** and paste it into the **Plaud token** field (the leading `Bearer ` is optional — the plugin adds it) + +Prefer the console? Paste this one-liner, then click around the app (or reload) and copy the value it logs: + +```js +(()=>{const L=(s,v)=>console.log('[plaud '+s+'] '+v);const of=window.fetch;window.fetch=function(i,init){try{const h=init&&init.headers;const a=h&&(typeof h.get==='function'?h.get('authorization'):h.Authorization||h.authorization);if(a)L('fetch',a)}catch(e){}return of.apply(this,arguments)};const o=XMLHttpRequest.prototype.setRequestHeader;XMLHttpRequest.prototype.setRequestHeader=function(n,v){if(/^authorization$/i.test(n))L('xhr',v);return o.apply(this,arguments)};console.log('Armed - reload the file list')})() +``` + +The access token is short-lived (it expires after roughly a day), so when sync starts failing with an authentication error, repeat these steps to grab a fresh one. ## Configuration diff --git a/src/main.ts b/src/main.ts index a9a52d1..8a53af1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,7 +24,7 @@ function toErrorMessage(error: unknown): string { function toActionableMessage(error: unknown): string { if (error instanceof PlaudApiError) { if (error.category === 'auth') { - return 'authentication failed. Re-save your Plaud token in settings.'; + return 'authentication failed: your Plaud token is invalid or expired. Extract a fresh token (see README) and re-save it in settings.'; } if (error.category === 'rate_limit') { return 'rate limited by Plaud API. Wait briefly and retry.'; diff --git a/src/plaud-api.ts b/src/plaud-api.ts index 1e99ebf..51acf67 100644 --- a/src/plaud-api.ts +++ b/src/plaud-api.ts @@ -108,12 +108,29 @@ function mapStatusCategory(status: number): PlaudApiErrorCategory { return 'network'; } +function isAuthFailureEnvelope(envelope: PlaudEnvelope): boolean { + // Plaud rejects a bad/expired/wrong token with HTTP 200 and an in-body + // status code (e.g. -3900 "invalid auth header"). Treat those as auth + // failures rather than malformed responses so the user gets an actionable + // "re-save your token" message instead of "retry". + if (envelope.status === -3900) { + return true; + } + + const msg = typeof envelope.msg === 'string' ? envelope.msg.toLowerCase() : ''; + return /(auth|token|unauthor|forbidden|not logged|sign ?in|log ?in)/.test(msg); +} + function assertSuccessStatusIfPresent(envelope: PlaudEnvelope): void { if (!('status' in envelope)) { return; } if (!isSuccessStatus(envelope.status)) { + if (isAuthFailureEnvelope(envelope)) { + throw new PlaudApiError('auth', toErrorMessage(envelope.msg, 'Plaud authentication failed. Re-save a fresh token.')); + } + throw new PlaudApiError('invalid_response', toErrorMessage(envelope.msg, 'Plaud API returned non-success status.')); } } diff --git a/test/plaud-api-client.test.mjs b/test/plaud-api-client.test.mjs index 1e28972..bfbc4c1 100644 --- a/test/plaud-api-client.test.mjs +++ b/test/plaud-api-client.test.mjs @@ -152,6 +152,19 @@ test('maps invalid response shape to invalid_response category', async () => { ); }); +test('maps plaud auth-rejection envelope (HTTP 200, status -3900) to auth category', async () => { + const client = createPlaudApiClient({ + apiDomain: 'https://api.plaud.ai', + token: 'tok_123', + request: async () => ({status: 200, json: {status: -3900, msg: 'invalid auth header'}}) + }); + + await assert.rejects( + () => client.listFiles(), + (error) => error instanceof PlaudApiError && error.category === 'auth' + ); +}); + test('maps network exception to network category', async () => { const client = createPlaudApiClient({ apiDomain: 'https://api.plaud.ai',