From 8c812d37fb76801bbff445fff9845092e8b44af5 Mon Sep 17 00:00:00 2001 From: Hugo Carneiro Date: Thu, 9 Apr 2026 17:15:39 +0100 Subject: [PATCH 1/8] feat(login): migrate to unified /v1/auth/login?return=callback flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the legacy /v1/auth/third-party endpoint (which is being removed from the API as part of DEV-908) with the same unified sign-in URL the Fliplet Login widget and VS Code extension now use. The allow-list on the API already includes http://localhost:9001/callback so no server-side change is needed for the CLI path. Contract change: the callback URL now receives ?token=&user=... instead of ?auth_token=. Parse the token with url.parse(...).query so we handle any additional params the API may append. Drive-by fixes to the callback handler surfaced while testing: - Multi-org users no longer hard-fail. The previous code rejected any user belonging to more than one organization with "You belong to multiple organizations." Now we pick the first organization by default and print a note telling the user how to switch afterwards via `fliplet list-organizations` + `fliplet organization `. - Every error path now writes an HTTP response to the browser AND calls process.exit(1). The old code left connections hanging, which caused the browser to retry and fire the callback handler twice — leading to the "multiple organizations" error printing twice. package-lock.json: drift from 7.0.0 to 7.0.1 (matches package.json, unrelated to this change). Co-Authored-By: Claude Opus 4.6 (1M context) --- fliplet-login.js | 54 +++++++++++++++++++++++++++++++++++++---------- package-lock.json | 2 +- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/fliplet-login.js b/fliplet-login.js index 4ecdd423..897c2740 100644 --- a/fliplet-login.js +++ b/fliplet-login.js @@ -1,6 +1,6 @@ require('colors'); -const _ = require('lodash'); +const url = require('url'); const auth = require('./lib/auth'); const config = require('./lib/config'); @@ -20,7 +20,16 @@ ${'Press Ctrl+C to exit.'.blue} const port = 9001; const baseUrl = `http://localhost:${port}`; -const redirectUrl = `${config.api_url}v1/auth/third-party?redirect=${encodeURIComponent(`${baseUrl}/callback`)}&responseType=code&source=CLI&title=Sign%20in%20to%20Authorize%20the%20Fliplet%20CLI`; +const callbackUrl = `${baseUrl}/callback`; + +// New unified sign-in URL — replaces the legacy /v1/auth/third-party flow. +// The page validates the callback URL against an allow-list (which already +// includes http://localhost:9001/callback) and on success navigates the +// browser to ?token=XXX&user=. +const redirectUrl = `${config.api_url}v1/auth/login` + + `?return=callback` + + `&callback=${encodeURIComponent(callbackUrl)}` + + `&source=CLI`; require('http').createServer(function(req, res) { if (req.url.match(/login/)) { @@ -38,18 +47,30 @@ require('http').createServer(function(req, res) { } if (req.url.match(/callback/)) { - const authToken = _.last(req.url.split('auth_token=')); + // Parse the token (and optional user payload) from the callback URL. + // The unified contract delivers them as `?token=...&user=`. + const parsed = url.parse(req.url, true); + const authToken = parsed.query.token; + + if (!authToken) { + console.error('No auth token received from the sign-in flow.'); + res.writeHead(400); + res.end('Missing token'); + return process.exit(1); + } auth.setUserForToken(authToken).then(function(user) { organizations.getOrganizationsList().then(function onGetOrganizations(organizations) { if (!organizations.length) { - return console.error('Your organization has not been found.'); - } - - if (organizations.length > 1) { - return console.error('You belong to multiple organizations.'); + console.error('Your organization has not been found.'); + res.writeHead(400); + res.end('No organizations available for this account.'); + return process.exit(1); } + // For users belonging to multiple organizations, default to the + // first one. They can switch later via `fliplet organization ` + // (use `fliplet list-organizations` to see all available IDs). const userOrganization = organizations[0]; config.set('organization', userOrganization); @@ -57,18 +78,29 @@ require('http').createServer(function(req, res) { console.log('----------------------------------------------------------\r\n'); console.log(`You have logged in successfully. Welcome back, ${user.fullName.yellow.underline}!`); console.log(`Your organization has been set to ${userOrganization.name.green.underline} (#${userOrganization.id}). Your account email is ${user.email.yellow.underline}.`); - console.log(`You can now develop and publish components via the ${'fliplet run'.bgBlack.red} command! -`); + + if (organizations.length > 1) { + console.log(`\n${'Note'.yellow}: you belong to ${organizations.length} organizations. To switch, run ${'fliplet list-organizations'.bgBlack.red} to see all of them and ${'fliplet organization '.bgBlack.red} to change.`); + } + + console.log(`\nYou can now develop and publish components via the ${'fliplet run'.bgBlack.red} command!\n`); res.writeHead(302, { 'Location': `${baseUrl}/success?${Date.now()}` }); return res.end(); + }).catch(function(err) { + console.error('Failed to fetch organizations:', err); + res.writeHead(500); + res.end('Failed to fetch organizations'); + return process.exit(1); }); }).catch(function(err) { console.error(err); - process.exit(); + res.writeHead(500); + res.end('Authentication failed'); + return process.exit(1); }); } }).listen(9001); diff --git a/package-lock.json b/package-lock.json index 13b6d958..cacc83f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "fliplet-cli", - "version": "7.0.0", + "version": "7.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { From eb9a6c11a8c41106d7646bb8e48cd9a3ed44d7c0 Mon Sep 17 00:00:00 2001 From: Hugo Carneiro Date: Tue, 14 Apr 2026 19:37:50 +0100 Subject: [PATCH 2/8] fix(organizations): read auth_token lazily instead of at module load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The organizations module captured auth_token at require-time. When `fliplet login` runs as a single process — sign-in happens via a browser callback AFTER the module was loaded — the captured token was whatever was in configstore at startup (null on first login), and the subsequent getOrganizationsList() call failed with "You must login first with: fliplet login". Moving the user/auth_token lookup inside the function makes it read the up-to-date configstore value every call. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/organizations.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/organizations.js b/lib/organizations.js index c2e64a71..1b572a4b 100644 --- a/lib/organizations.js +++ b/lib/organizations.js @@ -2,11 +2,15 @@ const request = require('request'); const config = require('./config'); var organizationsList; -var user = config.data.user || {}; -var auth_token = user.auth_token; function getOrganizationsListFromServer() { return new Promise(function(resolve, reject) { + // Read the auth token lazily — sign-in may have happened after + // this module was loaded, in which case the cached user object + // from module-init time would be stale. + var user = config.data.user || {}; + var auth_token = user.auth_token; + if (!auth_token) { return reject('You must login first with: fliplet login'); } From 0c20e20c667378a00990dda83dda6010effa184f Mon Sep 17 00:00:00 2001 From: Hugo Carneiro Date: Tue, 14 Apr 2026 19:40:02 +0100 Subject: [PATCH 3/8] docs(api): add Fliplet.Auth JS APIs reference New developer documentation for the Fliplet.Auth SDK (fliplet-auth package). Covers: - signIn / signOut / currentUser / isSignedIn / getToken / onChange - Common patterns (gated screens, profile display, sign-out button, authenticated API requests, reacting to auth state changes) - What Fliplet.Auth does NOT do (no user creation from apps, no app-specific user data management) Also registers the doc in the API-Documentation.md index under the alphabetical "Auth" entry. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/API-Documentation.md | 1 + docs/API/fliplet-auth.md | 210 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 docs/API/fliplet-auth.md diff --git a/docs/API-Documentation.md b/docs/API-Documentation.md index 306f406c..e2657117 100644 --- a/docs/API-Documentation.md +++ b/docs/API-Documentation.md @@ -77,6 +77,7 @@ These JS APIs falls into the most common category and almost all apps, component - [Audio](API/fliplet-audio.md) (`fliplet-audio`) - [Audio Player](API/fliplet-audio-player.md) (`fliplet-audio-player`) + - [Auth](API/fliplet-auth.md) (`fliplet-auth`) - [Barcode](API/fliplet-barcode.md) (`fliplet-barcode`) - [Communicate](API/fliplet-communicate.md) (`fliplet-communicate`) - [Content](API/fliplet-content.md) (`fliplet-content`) diff --git a/docs/API/fliplet-auth.md b/docs/API/fliplet-auth.md new file mode 100644 index 00000000..52843e66 --- /dev/null +++ b/docs/API/fliplet-auth.md @@ -0,0 +1,210 @@ +# Auth JS APIs + +The Fliplet Auth JS APIs let you sign users into your Fliplet app with one line of code. It opens the unified sign-in popup (served by the Fliplet API) and handles the round-trip for you — including email/password and SSO (Google, Microsoft, Apple). + +Use these APIs to: + +- Sign users into your app with their Fliplet account +- Read the currently signed-in user +- Sign users out +- Get the auth token for custom API calls +- React to sign-in / sign-out events + +Add the `fliplet-auth` package to your app's dependencies to enable these APIs. + +--- + +## Methods + +### Sign a user in + +Opens the unified sign-in popup. Resolves with `{ user, token }` when the user successfully signs in. + +```js +Fliplet.Auth.signIn().then(function(result) { + console.log('Signed in as', result.user.email); + console.log('Auth token:', result.token); +}); +``` + +Pre-fill the email field in the sign-in form: + +```js +Fliplet.Auth.signIn({ email: 'user@example.com' }); +``` + +The promise rejects with an `Error` when: + +- The popup is blocked by the browser +- The user closes the popup without completing sign-in +- The sign-in times out (10 minutes) +- The API returns an error (invalid credentials, 2FA flow cannot complete, etc.) + +```js +Fliplet.Auth.signIn().then(function(result) { + // user signed in +}).catch(function(err) { + console.error(err.message); +}); +``` + +--- + +### Get the currently signed-in user + +Resolves to the signed-in user, or `null` when no user is signed in. + +```js +Fliplet.Auth.currentUser().then(function(user) { + if (!user) { + // not signed in + return; + } + + // contains id, email, userRoleId, region + console.log(user); +}); +``` + +--- + +### Check whether a user is signed in + +A convenience helper that resolves to a boolean. + +```js +Fliplet.Auth.isSignedIn().then(function(isSignedIn) { + if (isSignedIn) { + // user is signed in + } +}); +``` + +--- + +### Get the user's auth token + +Resolves to the signed-in user's auth token, or `null` when no user is signed in. Use it to authenticate custom API calls as the signed-in user. + +```js +Fliplet.Auth.getToken().then(function(token) { + return fetch(url, { + headers: { 'Auth-Token': token } + }); +}); +``` + +--- + +### Sign the user out + +Clears locally stored credentials and invalidates the server-side session. + +```js +Fliplet.Auth.signOut().then(function() { + // user is signed out +}); +``` + +--- + +### Listen for sign-in / sign-out events + +Subscribe to auth state changes. The callback fires with the new user after a successful sign-in, or with `null` after sign-out completes. + +```js +var unsubscribe = Fliplet.Auth.onChange(function(user) { + if (user) { + // user just signed in + } else { + // user just signed out + } +}); + +// Stop listening when you no longer need to +unsubscribe(); +``` + +--- + +## Examples + +### Gate a screen behind sign-in + +Prompt the user to sign in if they're not already signed in, then continue with the rest of the screen. + +```js +Fliplet.Auth.currentUser().then(function(user) { + if (user) return user; + return Fliplet.Auth.signIn().then(function(result) { + return result.user; + }); +}).then(function(user) { + // render the screen as this user +}); +``` + +### Show the signed-in user's name in the header + +```js +Fliplet.Auth.currentUser().then(function(user) { + var greeting = user + ? 'Welcome, ' + user.email + : 'Sign in to continue'; + + document.querySelector('.profile-greeting').textContent = greeting; +}); +``` + +### Sign-out button + +```js +document.querySelector('.sign-out-button').addEventListener('click', function() { + Fliplet.Auth.signOut().then(function() { + location.reload(); + }); +}); +``` + +### Refresh the UI when auth state changes + +Useful when the user signs in or out via a different mechanism (e.g. a Fliplet Login widget on another screen). + +```js +Fliplet.Auth.onChange(function(user) { + if (user) { + document.body.classList.add('signed-in'); + } else { + document.body.classList.remove('signed-in'); + } +}); +``` + +### Make an authenticated API request + +```js +Fliplet.Auth.getToken().then(function(token) { + if (!token) { + throw new Error('Not signed in'); + } + + return Fliplet.API.request({ + url: 'v1/user', + headers: { 'Auth-Token': token } + }); +}).then(function(response) { + console.log(response.user); +}); +``` + +--- + +## What `Fliplet.Auth` does NOT do + +- **It does not create new Fliplet user accounts from inside an app.** Only Studio's sign-up page can create Fliplet accounts. An app's sign-in popup can authenticate existing Fliplet users (including via SSO) — but it will not create a new account. Users who don't yet have a Fliplet account see an error message asking them to sign up at Fliplet Studio first. +- **It does not manage app-specific user data.** Use a Data Source with the [data source login component](components/login.md) or your own custom logic for that. `Fliplet.Auth` authenticates Fliplet users, not app-specific records. + +--- + +[Back to API documentation](../API-Documentation.md) +{: .buttons} From 41af33fb271ddcd5f752a47a8a2ba7a36939fc1d Mon Sep 17 00:00:00 2001 From: Hugo Carneiro Date: Tue, 14 Apr 2026 19:46:06 +0100 Subject: [PATCH 4/8] docs(api): apply fliplet-docs style guide to Fliplet.Auth page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review pass against references/style-guide.md and references/dev-docs-rules.md: - Add YAML frontmatter with description field (required for dev docs) - Remove all horizontal rules (---) per style guide - Promote the "can't create users from apps" caveat to a

callout in "Before you start" — previously buried at the end - Disambiguate Fliplet.Auth vs Fliplet.User vs data source login on first use rather than only mentioning at the end - First signIn() example includes .catch() so copy-paste is safe - Make Error rejection cases explicit with exact message strings - Remove `region` from the documented currentUser() shape (internal detail derived from token prefix, not part of public contract) - Clarify signIn() vs currentUser() return-shape asymmetry inline at the point of discovery rather than in a separate "gotchas" section - Add "if server-side logout fails" behaviour note under signOut() - All code examples guard against null token / missing user per dev-docs-rules ("Code examples must match documented constraints") Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/API/fliplet-auth.md | 138 +++++++++++++++++++++------------------ 1 file changed, 75 insertions(+), 63 deletions(-) diff --git a/docs/API/fliplet-auth.md b/docs/API/fliplet-auth.md index 52843e66..92376ffd 100644 --- a/docs/API/fliplet-auth.md +++ b/docs/API/fliplet-auth.md @@ -1,58 +1,59 @@ +--- +description: Sign users into your Fliplet app with one line of code — email/password or SSO (Google, Microsoft, Apple). +--- + # Auth JS APIs -The Fliplet Auth JS APIs let you sign users into your Fliplet app with one line of code. It opens the unified sign-in popup (served by the Fliplet API) and handles the round-trip for you — including email/password and SSO (Google, Microsoft, Apple). +The Fliplet Auth JS APIs sign users into your Fliplet app with their existing Fliplet account. One call opens a popup, handles the authentication round-trip, and resolves with the signed-in user. Email/password and SSO (Google, Microsoft, Apple) are supported out of the box. -Use these APIs to: +Use these APIs to authenticate Fliplet users, read the signed-in user in your app code, sign users out, and get the auth token for custom API requests. -- Sign users into your app with their Fliplet account -- Read the currently signed-in user -- Sign users out -- Get the auth token for custom API calls -- React to sign-in / sign-out events +## Before you start -Add the `fliplet-auth` package to your app's dependencies to enable these APIs. +Add the `fliplet-auth` package to your app's dependencies. This exposes `Fliplet.Auth.*` at runtime. ---- +

Fliplet.Auth authenticates existing Fliplet user accounts. It does not create new accounts from inside an app — only Fliplet Studio's sign-up page can do that. A user who tries to sign in with an SSO provider (Google, Microsoft, Apple) that isn't linked to any Fliplet account sees an error asking them to sign up at Fliplet Studio first. Direct users to Fliplet Studio to create their account, then back to your app to sign in.

+ +`Fliplet.Auth` is the high-level API for signing users in and out — prefer it for app code. It is distinct from two other APIs that share related concepts: + +- **`Fliplet.User`** — low-level primitives (`getAuthToken`, `setAuthToken`, `getCachedSession`). `Fliplet.Auth` uses these internally; you rarely need to call them directly. +- **Data source login component** — an app-level authentication system that uses a data source as the user table. It's a separate system from `Fliplet.Auth` and is appropriate when you want users stored in your app's own data source rather than in Fliplet's user database. See [data source login](components/login.md) for that. ## Methods ### Sign a user in -Opens the unified sign-in popup. Resolves with `{ user, token }` when the user successfully signs in. +`Fliplet.Auth.signIn()` opens the unified sign-in popup and resolves with `{ user, token }` when the user successfully signs in. ```js Fliplet.Auth.signIn().then(function(result) { + // result.user: { id, email, firstName, lastName, userRoleId } + // result.token: auth token string, e.g. "eu--55a54008...-203-8965" console.log('Signed in as', result.user.email); - console.log('Auth token:', result.token); +}).catch(function(err) { + // surface the error to the user and let them retry + console.error(err.message); }); ``` -Pre-fill the email field in the sign-in form: +Pre-fill the email field in the sign-in form by passing `options.email`: ```js -Fliplet.Auth.signIn({ email: 'user@example.com' }); +Fliplet.Auth.signIn({ email: 'alice@acme.com' }); ``` -The promise rejects with an `Error` when: - -- The popup is blocked by the browser -- The user closes the popup without completing sign-in -- The sign-in times out (10 minutes) -- The API returns an error (invalid credentials, 2FA flow cannot complete, etc.) +The promise **rejects with an `Error`** when: -```js -Fliplet.Auth.signIn().then(function(result) { - // user signed in -}).catch(function(err) { - console.error(err.message); -}); -``` +- The popup is blocked by the browser → `Error('Sign-in popup was blocked...')` +- The user closes the popup without completing sign-in → `Error('Sign-in was cancelled.')` +- Sign-in times out after 10 minutes → `Error('Sign-in timed out. Please try again.')` +- The API returns a sign-in error → `Error()` ---- +Always handle rejection in your app code — the examples on this page consistently do so. ### Get the currently signed-in user -Resolves to the signed-in user, or `null` when no user is signed in. +`Fliplet.Auth.currentUser()` resolves to the signed-in user, or `null` when no user is signed in. ```js Fliplet.Auth.currentUser().then(function(user) { @@ -61,86 +62,106 @@ Fliplet.Auth.currentUser().then(function(user) { return; } - // contains id, email, userRoleId, region - console.log(user); + // user shape: { id, email, userRoleId } + console.log(user.email); }); ``` ---- +The user object returned by `currentUser()` contains a subset of the fields returned by `signIn()` — specifically the fields that persist locally between sessions: `id`, `email`, `userRoleId`. To access `firstName` and `lastName`, either store them yourself after `signIn()` resolves, or query `v1/user` with `Fliplet.Auth.getToken()`. ### Check whether a user is signed in -A convenience helper that resolves to a boolean. +`Fliplet.Auth.isSignedIn()` resolves to `true` or `false` — a convenience helper on top of `currentUser()`. ```js Fliplet.Auth.isSignedIn().then(function(isSignedIn) { if (isSignedIn) { - // user is signed in + // show authenticated UI } }); ``` ---- - ### Get the user's auth token -Resolves to the signed-in user's auth token, or `null` when no user is signed in. Use it to authenticate custom API calls as the signed-in user. +`Fliplet.Auth.getToken()` resolves to the signed-in user's auth token, or `null` when no user is signed in. Use it to authenticate custom API calls as the signed-in user. ```js Fliplet.Auth.getToken().then(function(token) { - return fetch(url, { + if (!token) { + throw new Error('Not signed in'); + } + + return fetch('https://api.example.com/my-endpoint', { headers: { 'Auth-Token': token } }); }); ``` ---- +Pair with `Fliplet.API.request()` for calls to the Fliplet API: + +```js +Fliplet.Auth.getToken().then(function(token) { + return Fliplet.API.request({ + url: 'v1/user', + headers: { 'Auth-Token': token } + }); +}).then(function(response) { + console.log(response.user); +}); +``` ### Sign the user out -Clears locally stored credentials and invalidates the server-side session. +`Fliplet.Auth.signOut()` clears locally stored credentials and invalidates the server-side session. ```js Fliplet.Auth.signOut().then(function() { - // user is signed out + // user is signed out; currentUser() now resolves to null }); ``` ---- +If the server-side logout request fails (for example, the device is offline), local credentials are still cleared so the user is effectively signed out. ### Listen for sign-in / sign-out events -Subscribe to auth state changes. The callback fires with the new user after a successful sign-in, or with `null` after sign-out completes. +`Fliplet.Auth.onChange(callback)` subscribes to auth state changes. The callback fires with the new user after a successful sign-in, or with `null` after sign-out completes. Returns an unsubscribe function. ```js var unsubscribe = Fliplet.Auth.onChange(function(user) { if (user) { - // user just signed in + // user just signed in — refresh the UI as this user } else { - // user just signed out + // user just signed out — reset to logged-out state } }); -// Stop listening when you no longer need to +// Stop listening when no longer needed unsubscribe(); ``` ---- +Useful when the user signs in or out via a different mechanism — for example, a Fliplet Login component on another screen. ## Examples ### Gate a screen behind sign-in -Prompt the user to sign in if they're not already signed in, then continue with the rest of the screen. +Prompt the user to sign in if they're not already signed in, then render the screen. ```js Fliplet.Auth.currentUser().then(function(user) { - if (user) return user; + if (user) { + return user; + } + return Fliplet.Auth.signIn().then(function(result) { return result.user; }); }).then(function(user) { // render the screen as this user + document.querySelector('.greeting').textContent = 'Welcome, ' + user.email; +}).catch(function(err) { + // user cancelled sign-in or it failed + console.error(err.message); }); ``` @@ -149,8 +170,8 @@ Fliplet.Auth.currentUser().then(function(user) { ```js Fliplet.Auth.currentUser().then(function(user) { var greeting = user - ? 'Welcome, ' + user.email - : 'Sign in to continue'; + ? 'Signed in as ' + user.email + : 'Not signed in'; document.querySelector('.profile-greeting').textContent = greeting; }); @@ -168,8 +189,6 @@ document.querySelector('.sign-out-button').addEventListener('click', function() ### Refresh the UI when auth state changes -Useful when the user signs in or out via a different mechanism (e.g. a Fliplet Login widget on another screen). - ```js Fliplet.Auth.onChange(function(user) { if (user) { @@ -189,22 +208,15 @@ Fliplet.Auth.getToken().then(function(token) { } return Fliplet.API.request({ - url: 'v1/user', + url: 'v1/organizations', headers: { 'Auth-Token': token } }); }).then(function(response) { - console.log(response.user); + console.log('Organizations:', response.organizations); +}).catch(function(err) { + console.error(err); }); ``` ---- - -## What `Fliplet.Auth` does NOT do - -- **It does not create new Fliplet user accounts from inside an app.** Only Studio's sign-up page can create Fliplet accounts. An app's sign-in popup can authenticate existing Fliplet users (including via SSO) — but it will not create a new account. Users who don't yet have a Fliplet account see an error message asking them to sign up at Fliplet Studio first. -- **It does not manage app-specific user data.** Use a Data Source with the [data source login component](components/login.md) or your own custom logic for that. `Fliplet.Auth` authenticates Fliplet users, not app-specific records. - ---- - [Back to API documentation](../API-Documentation.md) {: .buttons} From 004d605a88b35946889c894b22be32a9ecacd40b Mon Sep 17 00:00:00 2001 From: Hugo Carneiro Date: Tue, 14 Apr 2026 20:12:59 +0100 Subject: [PATCH 5/8] docs(sidebar): add Auth JS API link in the libraries sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a link to the Fliplet.Auth reference doc in the JS API → Libraries section of the sidebar, alphabetically between Core and Communicate, with the NEW label. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/_layouts/default.html | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 4b8fdf39..247359e7 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -203,6 +203,7 @@
  • Overview
  • Libraries

  • Core
  • +
  • Auth NEW
  • Communicate
  • Data Sources
  • Encryption
  • From ca4ed3c353fcb4b07e623742152714f216ca6b1c Mon Sep 17 00:00:00 2001 From: Hugo Carneiro Date: Mon, 15 Jun 2026 11:30:24 +0100 Subject: [PATCH 6/8] fix(login): route on parsed pathname to avoid callback misrouting (PS review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unified callback URL carries the signed-in user's data in the query string (`?token=...&user=`). The local callback server routed with loose `req.url.match(/login|success|callback/)` against the whole URL, so any user whose email or name contains "login" or "success" (e.g. john.login@acme.com, anyone named "Success") would match the earlier /login or /success branch instead of /callback — breaking their sign-in deterministically (redirect loop, or a silent success page that exits without ever storing the token). The old `?auth_token=`-only callback didn't carry user data, so the migration to the `&user=` payload is what exposed this. Fix: parse req.url once at the top and switch on the exact `pathname` (`/login`, `/success`, `/callback`) instead of substring-matching the full URL. No behaviour change for the happy path; removes the collision surface entirely. Co-Authored-By: Claude Opus 4.8 --- fliplet-login.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/fliplet-login.js b/fliplet-login.js index 897c2740..417f587c 100644 --- a/fliplet-login.js +++ b/fliplet-login.js @@ -32,7 +32,16 @@ const redirectUrl = `${config.api_url}v1/auth/login` + `&source=CLI`; require('http').createServer(function(req, res) { - if (req.url.match(/login/)) { + // Route on the parsed pathname, NOT a loose `req.url.match(/.../)`. The + // unified callback carries the signed-in user's data in the query string + // (`?token=...&user=`), so a substring match against the + // whole URL would misroute any user whose email or name contains "login" + // or "success" (e.g. john.login@acme.com, anyone named "Success") into the + // wrong branch — breaking their sign-in. Pathname matching is exact. + const parsed = url.parse(req.url, true); + const pathname = parsed.pathname; + + if (pathname === '/login') { res.writeHead(302, { 'Location': redirectUrl }); @@ -40,16 +49,15 @@ require('http').createServer(function(req, res) { return res.end(); } - if (req.url.match(/success/)) { + if (pathname === '/success') { res.end(`

    You have been logged in successfully to your Fliplet account.

    You may now close this window now and return to the terminal to continue the process.

    `); return process.exit(); } - if (req.url.match(/callback/)) { - // Parse the token (and optional user payload) from the callback URL. - // The unified contract delivers them as `?token=...&user=`. - const parsed = url.parse(req.url, true); + if (pathname === '/callback') { + // Token (and optional user payload) come from the callback query string. + // The unified contract delivers them as `?token=...&user=`. const authToken = parsed.query.token; if (!authToken) { From bbfa5e5f956cc289170d430534994b899b4925e8 Mon Sep 17 00:00:00 2001 From: Hugo Carneiro Date: Mon, 15 Jun 2026 12:06:02 +0100 Subject: [PATCH 7/8] docs(auth): fix token-leak example, onChange scope, currentUser shape (review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of the Fliplet.Auth docs against the shipped public/assets/fliplet-auth/1.0/auth.js on fliplet-api master: - Security: the getToken() example sent the Fliplet Auth-Token to a third-party origin (https://api.example.com). That token grants full access to the user's Fliplet account — sending it off the Fliplet origin is a credential leak. Replaced with a Fliplet.API.request call to the Fliplet API and added a warning callout (treat like a password; only ever send it to the Fliplet API; use a backend-issued token for your own backend). Removed the now-duplicate example. - Correctness: onChange only fires for Fliplet.Auth.signIn()/signOut() (notifyChangeListeners is module-private). The old copy claimed it was useful for sign-ins via "a different mechanism (Fliplet Login component on another screen)" — it does not fire for those. Reworded as a quote callout stating the real scope. - Correctness: currentUser() also returns `region` and `legacy` (readStoredUser, auth.js:80-86); the documented shape listed only id/email/userRoleId. Added the two fields. Co-Authored-By: Claude Opus 4.8 --- docs/API/fliplet-auth.md | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/docs/API/fliplet-auth.md b/docs/API/fliplet-auth.md index 92376ffd..56864aa5 100644 --- a/docs/API/fliplet-auth.md +++ b/docs/API/fliplet-auth.md @@ -62,12 +62,12 @@ Fliplet.Auth.currentUser().then(function(user) { return; } - // user shape: { id, email, userRoleId } + // user shape: { id, email, userRoleId, region, legacy } console.log(user.email); }); ``` -The user object returned by `currentUser()` contains a subset of the fields returned by `signIn()` — specifically the fields that persist locally between sessions: `id`, `email`, `userRoleId`. To access `firstName` and `lastName`, either store them yourself after `signIn()` resolves, or query `v1/user` with `Fliplet.Auth.getToken()`. +The user object returned by `currentUser()` contains the fields that persist locally between sessions: `id`, `email`, `userRoleId`, `region`, and `legacy`. It does **not** include `firstName` / `lastName` (which `signIn()` returns) — to access those, either store them yourself after `signIn()` resolves, or query `v1/user` with `Fliplet.Auth.getToken()`. ### Check whether a user is signed in @@ -83,7 +83,9 @@ Fliplet.Auth.isSignedIn().then(function(isSignedIn) { ### Get the user's auth token -`Fliplet.Auth.getToken()` resolves to the signed-in user's auth token, or `null` when no user is signed in. Use it to authenticate custom API calls as the signed-in user. +`Fliplet.Auth.getToken()` resolves to the signed-in user's auth token, or `null` when no user is signed in. Use it to authenticate calls to the Fliplet API as the signed-in user. + +

    Only send the auth token to the Fliplet API. The token grants full access to this user's Fliplet account — treat it like a password. Never put it in the URL, log it, or send it to a third-party / non-Fliplet endpoint, as that hands the user's account to whoever receives it. To call your own backend, send a short-lived token your backend issues, not the Fliplet auth token.

    ```js Fliplet.Auth.getToken().then(function(token) { @@ -91,16 +93,6 @@ Fliplet.Auth.getToken().then(function(token) { throw new Error('Not signed in'); } - return fetch('https://api.example.com/my-endpoint', { - headers: { 'Auth-Token': token } - }); -}); -``` - -Pair with `Fliplet.API.request()` for calls to the Fliplet API: - -```js -Fliplet.Auth.getToken().then(function(token) { return Fliplet.API.request({ url: 'v1/user', headers: { 'Auth-Token': token } @@ -110,6 +102,8 @@ Fliplet.Auth.getToken().then(function(token) { }); ``` +`Fliplet.API.request()` is the recommended way to call the Fliplet API — it targets the correct API host for you and keeps the token on the Fliplet origin. + ### Sign the user out `Fliplet.Auth.signOut()` clears locally stored credentials and invalidates the server-side session. @@ -139,7 +133,7 @@ var unsubscribe = Fliplet.Auth.onChange(function(user) { unsubscribe(); ``` -Useful when the user signs in or out via a different mechanism — for example, a Fliplet Login component on another screen. +

    onChange fires only for sign-ins and sign-outs performed through Fliplet.Auth.signIn() and Fliplet.Auth.signOut(). It does not currently fire for auth changes made by other mechanisms (for example a Fliplet Login component on another screen), since those don't route through this API.

    ## Examples From e88bbc4a942287d0df460d68634fbe7eefbabba6 Mon Sep 17 00:00:00 2001 From: Hugo Carneiro Date: Mon, 15 Jun 2026 15:02:56 +0100 Subject: [PATCH 8/8] fix(cli): send auth token via header, not URL; drop dead source param (review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Zeryab's review on #237. Token transport (the substantial item): the CLI authenticated by putting the token in the URL query string, while the Fliplet.Auth doc this PR ships warns "never put it in the URL." Tokens in URLs leak into server / proxy / CDN access logs and Referer headers. Moved all five call sites to the `Auth-Token` header — the API reads `req.headers['auth-token']` first (libs/authenticate.js:518), ahead of the query param, so this is a safe swap for every endpoint: - lib/auth.js setUserForToken() → v1/user - lib/auth.js isAdmin() → v1/user - lib/organizations.js → v1/organizations (also switched the string-URL request() form to object form so it can carry headers) - lib/publish.js → v1/widgets (POST) - lib/template.js compile() → v1/widgets/compile (POST) Nits: - Drop the dead `&source=CLI` query param. The unified login page derives the session source from `isEmbedded` (views/login.pug) and never reads `?source=`, so it tagged nothing. Dropped on the CLI side rather than changing the API. - `.listen(9001)` → `.listen(port)` (the `port` const already existed). - Comment fix: callback `user` payload is url-encoded JSON, not base64. All five files pass `node --check`; no `?auth_token=` remains in any URL. Co-Authored-By: Claude Opus 4.8 --- fliplet-login.js | 11 +++++++---- lib/auth.js | 6 ++++-- lib/organizations.js | 5 ++++- lib/publish.js | 3 ++- lib/template.js | 6 ++++-- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/fliplet-login.js b/fliplet-login.js index 417f587c..763da043 100644 --- a/fliplet-login.js +++ b/fliplet-login.js @@ -25,11 +25,14 @@ const callbackUrl = `${baseUrl}/callback`; // New unified sign-in URL — replaces the legacy /v1/auth/third-party flow. // The page validates the callback URL against an allow-list (which already // includes http://localhost:9001/callback) and on success navigates the -// browser to ?token=XXX&user=. +// browser to ?token=XXX&user=. +// +// No `&source=CLI`: the unified login page derives the session source from +// `isEmbedded` (views/login.pug) and does not read `?source=`, so passing it +// here would be a dead param that never tags the session. const redirectUrl = `${config.api_url}v1/auth/login` + `?return=callback` - + `&callback=${encodeURIComponent(callbackUrl)}` - + `&source=CLI`; + + `&callback=${encodeURIComponent(callbackUrl)}`; require('http').createServer(function(req, res) { // Route on the parsed pathname, NOT a loose `req.url.match(/.../)`. The @@ -111,6 +114,6 @@ require('http').createServer(function(req, res) { return process.exit(1); }); } -}).listen(9001); +}).listen(port); require('openurl').open(`${baseUrl}/login?${Date.now()}`); diff --git a/lib/auth.js b/lib/auth.js index d544b922..196d08fa 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -28,7 +28,8 @@ function login(options) { function setUserForToken(authToken) { return new Promise(function(resolve, reject) { request({ - url: config.api_url + 'v1/user?auth_token=' + authToken + url: config.api_url + 'v1/user', + headers: { 'Auth-Token': authToken } }, function(error, response, body) { if (error) { return reject({ error, response, body }); @@ -64,7 +65,8 @@ function isAdmin() { return new Promise(function(resolve, reject) { request({ method: 'GET', - url: `${config.api_url}v1/user?auth_token=${auth_token}` + url: `${config.api_url}v1/user`, + headers: { 'Auth-Token': auth_token } }, function(error, response, body) { if (error) { reject(error); diff --git a/lib/organizations.js b/lib/organizations.js index 1b572a4b..24591f94 100644 --- a/lib/organizations.js +++ b/lib/organizations.js @@ -19,7 +19,10 @@ function getOrganizationsListFromServer() { return resolve(organizationsList); } - request(config.api_url + 'v1/organizations?auth_token=' + auth_token, function(error, response, body) { + request({ + url: config.api_url + 'v1/organizations', + headers: { 'Auth-Token': auth_token } + }, function(error, response, body) { if (error) { return reject(error); } diff --git a/lib/publish.js b/lib/publish.js index 2356c392..3fdfb59f 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -62,7 +62,8 @@ function publish(user, opts = {}) { const options = { method: 'POST', json: true, - url: config.api_url + 'v1/widgets?auth_token=' + auth_token, + url: config.api_url + 'v1/widgets', + headers: { 'Auth-Token': auth_token }, formData: formData, timeout: 1000 * 60 * 5 // 5 minutes }; diff --git a/lib/template.js b/lib/template.js index cb810796..28899262 100644 --- a/lib/template.js +++ b/lib/template.js @@ -4,17 +4,19 @@ const request = require('request'); const config = require('./config'); function compile(options) { - let url = config.api_url + 'v1/widgets/compile'; + const url = config.api_url + 'v1/widgets/compile'; const user = config.get('user'); + const headers = {}; if (user && user.authToken) { - url += `?auth_token=${user.authToken}`; + headers['Auth-Token'] = user.authToken; } return new Promise(function(resolve, reject) { request({ method: 'POST', url, + headers, gzip: true, json: options }, function(error, response, body) {