Skip to content
Open
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
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.';
Expand Down
17 changes: 17 additions & 0 deletions src/plaud-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,29 @@ function mapStatusCategory(status: number): PlaudApiErrorCategory {
return 'network';
}

function isAuthFailureEnvelope(envelope: PlaudEnvelope<unknown>): 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<unknown>): 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.'));
}
}
Expand Down
13 changes: 13 additions & 0 deletions test/plaud-api-client.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down