@hotosm/hanko-auth 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # Web Component: `<hotosm-auth>`
2
2
 
3
- Lit-based web component for HOTOSM SSO authentication with Hanko and OpenStreetMap integration.
3
+ Lit-based web component for HOTOSM SSO authentication with Hanko and
4
+ OpenStreetMap integration.
4
5
 
5
6
  ## Installation
6
7
 
@@ -56,57 +57,57 @@ export function AuthButton({ hankoUrl, onLogin }) {
56
57
 
57
58
  ### Core
58
59
 
59
- | Attribute | Type | Default | Description |
60
- | ----------- | ------ | ------------------------ | ------------------------------------------ |
61
- | `hanko-url` | string | `window.location.origin` | Login service URL for Hanko authentication |
62
- | `base-path` | string | `""` | Base URL for OSM OAuth endpoints |
63
- | `auth-path` | string | `/api/auth/osm` | OSM auth endpoints path |
60
+ | Attribute | Type | Default | Description |
61
+ | - | - | - | - |
62
+ | `hanko-url` | string | `window.location.origin` | Login service URL |
63
+ | `base-path` | string | `""` | Base URL for OSM OAuth endpoints |
64
+ | `auth-path` | string | `/api/auth/osm` | OSM auth endpoints path |
64
65
 
65
66
  ### Behavior
66
67
 
67
- | Attribute | Type | Default | Description |
68
- | ---------------- | ------- | -------------- | -------------------------- |
69
- | `osm-required` | boolean | `false` | Require OSM connection |
70
- | `osm-scopes` | string | `"read_prefs"` | Space-separated OSM scopes |
71
- | `auto-connect` | boolean | `false` | Auto-redirect to OSM OAuth |
72
- | `verify-session` | boolean | `false` | Verify session on return |
68
+ | Attribute | Type | Default | Description |
69
+ | - | - | - | - |
70
+ | `osm-required` | boolean | `false` | Require OSM connection |
71
+ | `osm-scopes` | string | `"read_prefs"` | OSM scopes (space-separated) |
72
+ | `auto-connect` | boolean | `false` | Auto redirect to OSM OAuth |
73
+ | `verify-session` | boolean | `false` | Verify session on return flow |
73
74
 
74
75
  ### Display
75
76
 
76
- | Attribute | Type | Default | Description |
77
- | ---------------- | ------- | ---------- | ----------------------------------------------------------------- |
78
- | `show-profile` | boolean | `false` | Show full profile (vs header button) |
79
- | `display-name` | string | `""` | Override display name |
80
- | `lang` | string | `"en"` | Language/locale code (e.g., "en", "es", "fr"). Enlish as fallback |
81
- | `button-variant` | string | `"filled"` | Button style: `filled`, `outline`, or `plain` |
82
- | `button-color` | string | `"primary"`| Button color: `primary`, `neutral`, or `danger` |
83
- | `display` | string | `"default"`| Display mode: `default` (compact avatar) or `bar` (full-width bar with avatar + email + arrow) |
77
+ | Attribute | Type | Default | Description |
78
+ | - | - | - | - |
79
+ | `show-profile` | boolean | `false` | Show full profile |
80
+ | `display-name` | string | `""` | Override display name |
81
+ | `lang` | string | `"en"` | Locale (e.g., "en", "es", "fr"), fallback "en" |
82
+ | `button-variant` | string | `"filled"` | `filled`, `outline`, or `plain` |
83
+ | `button-color` | string | `"primary"` | `primary`, `neutral`, or `danger` |
84
+ | `display` | string | `"default"` | `default` (avatar) or `bar` mode |
84
85
 
85
86
  ### Redirects
86
87
 
87
- | Attribute | Type | Default | Description |
88
+ | Attribute | Type | Default | Description |
88
89
  | ----------------------- | ------ | ------- | -------------------------- |
89
- | `redirect-after-login` | string | `""` | URL after successful login |
90
- | `redirect-after-logout` | string | `""` | URL after logout |
90
+ | `redirect-after-login` | string | `""` | URL after successful login |
91
+ | `redirect-after-logout` | string | `""` | URL after logout |
91
92
 
92
93
  ### Cross-app
93
94
 
94
- | Attribute | Type | Default | Description |
95
+ | Attribute | Type | Default | Description |
95
96
  | ------------------- | ------ | ------- | ----------------------------- |
96
- | `mapping-check-url` | string | `""` | URL to check user mapping |
97
- | `app-id` | string | `""` | App identifier for onboarding |
97
+ | `mapping-check-url` | string | `""` | URL to check user mapping |
98
+ | `app-id` | string | `""` | App identifier for onboarding |
98
99
 
99
100
  ## Events
100
101
 
101
102
  The component dispatches the following custom events:
102
103
 
103
- | Event | Detail | When |
104
+ | Event | Detail | When |
104
105
  | --------------- | ---------------------- | --------------------------- |
105
- | `hanko-login` | `{ user: HankoUser }` | User logged in |
106
- | `osm-connected` | `{ osmData: OSMData }` | OSM account linked |
107
- | `osm-skipped` | `{}` | User skipped OSM connection |
108
- | `auth-complete` | `{}` | Auth flow complete |
109
- | `logout` | `{}` | User logged out |
106
+ | `hanko-login` | `{ user: HankoUser }` | User logged in |
107
+ | `osm-connected` | `{ osmData: OSMData }` | OSM account linked |
108
+ | `osm-skipped` | `{}` | User skipped OSM connection |
109
+ | `auth-complete` | `{}` | Auth flow complete |
110
+ | `logout` | `{}` | User logged out |
110
111
 
111
112
  ### Event Handling Example
112
113
 
@@ -129,9 +130,12 @@ auth.addEventListener("logout", () => {
129
130
 
130
131
  ## Flash Prevention (localStorage cache)
131
132
 
132
- On remount (e.g. React navigation), the component checks `localStorage` for a cached user under the key `hotosm-auth-user`. If found, it skips the loading spinner and renders immediately with the cached user.
133
+ On remount (e.g. React navigation), the component checks `localStorage` for a
134
+ cached user under the key `hotosm-auth-user`. If found, it skips the loading
135
+ spinner and renders immediately with the cached user.
133
136
 
134
- The component reads from this key but does not write to it. The host app is responsible for keeping it in sync:
137
+ The component reads from this key but does not write to it. The host app is
138
+ responsible for keeping it in sync:
135
139
 
136
140
  ```js
137
141
  // Write on login
@@ -145,7 +149,8 @@ auth.addEventListener("logout", () => {
145
149
  });
146
150
  ```
147
151
 
148
- If the key is absent (first visit, after logout, or cleared storage), the component falls back to its normal loading flow — no change in behavior.
152
+ If the key is absent (first visit, after logout, or cleared storage), the
153
+ component falls back to its normal loading flow - no change in behavior.
149
154
 
150
155
  ## Usage Modes
151
156
 
@@ -194,7 +199,8 @@ Customize the login button appearance with `button-variant` and `button-color`:
194
199
 
195
200
  ### Bar Mode
196
201
 
197
- Shows a full-width bar with avatar, email, and chevron arrow (ideal for mobile drawers/menus):
202
+ Shows a full-width bar with avatar, email, and chevron arrow (ideal for mobile
203
+ drawers/menus):
198
204
 
199
205
  ```html
200
206
  <hotosm-auth
@@ -226,26 +232,26 @@ The component uses Shadow DOM and can be customized using CSS custom properties.
226
232
 
227
233
  #### Whole component
228
234
 
229
- | Property | Description | Default |
230
- | ----------------- | --------------------------------------------- | --------------------------------------- |
231
- | `--font-family` | Font family for all text in the component | `system-ui, -apple-system, sans-serif` |
232
- | `--font-weight` | Font weight for all text in the component | `500` |
235
+ | Property | Description | Default |
236
+ | - | - | - |
237
+ | `--font-family` | Font family for all text | `system-ui` |
238
+ | `--font-weight` | Font weight for all text | `500` |
233
239
 
234
240
  #### Login button
235
241
 
236
- | Property | Description | Default |
237
- | ----------------------------- | ---------------------------------- | ------------------------------------------------------ |
238
- | `--login-btn-margin` | Margin around the login button | `0` |
239
- | `--login-btn-padding` | Padding inside the login button | `var(--hot-spacing-x-small) var(--hot-spacing-medium)` |
240
- | `--login-btn-bg-color` | Background color of login button | `var(--hot-color-primary-1000)` |
241
- | `--login-btn-hover-bg-color` | Background color on hover | `var(--hot-color-primary-900)` |
242
- | `--login-btn-border-radius` | Border radius of login button | `var(--hot-border-radius-medium)` |
243
- | `--login-btn-text-color` | Text color of login button | `white` |
244
- | `--login-btn-text-size` | Font size of login button text | `var(--hot-font-size-medium)` |
245
- | `--login-btn-font-family` | Font family of login button | falls back to `--font-family` |
246
- | `--login-btn-font-weight` | Font weight of login button | falls back to `--font-weight` |
247
-
248
- **Example:**
242
+ | Property | Description | Default |
243
+ | - | - | - |
244
+ | `--login-btn-margin` | Margin around the login button | `0` |
245
+ | `--login-btn-padding` | Padding inside button | `x-small ...` |
246
+ | `--login-btn-bg-color` | Button bg color | `var(--hot-color-primary-1000)` |
247
+ | `--login-btn-hover-bg-color` | Hover bg | `primary-900` |
248
+ | `--login-btn-border-radius` | Button radius | `radius-medium` |
249
+ | `--login-btn-text-color` | Button text color | `white` |
250
+ | `--login-btn-text-size` | Button text size | `var(--hot-font-size-medium)` |
251
+ | `--login-btn-font-family` | Button font family | from `--font-family` |
252
+ | `--login-btn-font-weight` | Button font weight | from `--font-weight` |
253
+
254
+ ### Example
249
255
 
250
256
  ```css
251
257
  hotosm-auth {
@@ -4018,7 +4018,7 @@ const as = () => (nn && nn.abort(), nn = new AbortController(), nn.signal), Lt =
4018
4018
 
4019
4019
  .login-link {
4020
4020
  color: var(--login-btn-text-color, white);
4021
- font-size: var(--login-btn-text-size, var(--hot-font-size-medium));
4021
+ font-size: var(--hot-font-size-small);
4022
4022
  border-radius: var(
4023
4023
  --login-btn-border-radius,
4024
4024
  var(--hot-border-radius-medium)
@@ -4047,11 +4047,11 @@ const as = () => (nn && nn.abort(), nn = new AbortController(), nn.signal), Lt =
4047
4047
  border: none;
4048
4048
  }
4049
4049
  .login-link.filled.primary {
4050
- background: var(--login-btn-bg-color, var(--hot-color-primary-1000));
4050
+ background: var(--hot-color-gray-950);
4051
4051
  color: var(--login-btn-text-color, white);
4052
4052
  }
4053
4053
  .login-link.filled.primary:hover {
4054
- background: var(--login-btn-hover-bg-color, var(--hot-color-primary-900));
4054
+ background: var(--hot-color-primary-800);
4055
4055
  }
4056
4056
  .login-link.filled.neutral {
4057
4057
  background: var(--login-btn-bg-color, var(--hot-color-neutral-600));
@@ -4074,8 +4074,8 @@ const as = () => (nn && nn.abort(), nn = new AbortController(), nn.signal), Lt =
4074
4074
  border: 1px solid;
4075
4075
  }
4076
4076
  .login-link.outline.primary {
4077
- border-color: var(--login-btn-bg-color, var(--hot-color-primary-1000));
4078
- color: var(--login-btn-text-color, var(--hot-color-primary-1000));
4077
+ border-color: var(--login-btn-bg-color, var(--hot-color-primary-950));
4078
+ color: var(--login-btn-text-color, var(--hot-color-primary-950));
4079
4079
  }
4080
4080
  .login-link.outline.primary:hover {
4081
4081
  background: var(--login-btn-hover-bg-color, var(--hot-color-primary-50));
@@ -4101,7 +4101,7 @@ const as = () => (nn && nn.abort(), nn = new AbortController(), nn.signal), Lt =
4101
4101
  border: none;
4102
4102
  }
4103
4103
  .login-link.plain.primary {
4104
- color: var(--login-btn-text-color, var(--hot-color-primary-1000));
4104
+ color: var(--login-btn-text-color, var(--hot-color-primary-950));
4105
4105
  }
4106
4106
  .login-link.plain.primary:hover {
4107
4107
  background: var(--login-btn-hover-bg-color, var(--hot-color-primary-50));
@@ -4848,7 +4848,7 @@ let oe = class extends qt {
4848
4848
  constructor() {
4849
4849
  super(), this.hankoUrlAttr = "", this.basePath = "", this.authPath = "/api/auth/osm", this.osmRequired = !1, this.osmScopes = "read_prefs", this.showProfile = !1, this.redirectAfterLogin = "", this.autoConnect = !1, this.verifySession = !1, this.redirectAfterLogout = "", this.displayNameAttr = "", this.mappingCheckUrl = "", this.appId = "", this.loginUrl = "", this.lang = "en", this.buttonVariant = "plain", this.buttonColor = "primary", this.display = "default", this.user = null, this.osmConnected = !1, this.osmData = null, this.osmLoading = !1, this.loading = !0, this.error = null, this.hankoReady = !1, this.profileDisplayName = "", this.profilePictureUrl = "", this.hasAppMapping = !1, this.userProfileLanguage = null, this.isOpen = !1, this.handleOutsideClick = (n) => {
4850
4850
  this.contains(n.target) || this.closeDropdown();
4851
- }, this._debugMode = !1, this._lastSessionId = null, this._hanko = null, this._isPrimary = !1, this._hankoObserver = null, this._signUpHeadlines = /* @__PURE__ */ new Set([
4851
+ }, this._debugMode = !1, this._lastSessionId = null, this._hanko = null, this._isPrimary = !1, this._sessionCheckFailures = 0, this._sessionCheckBackoffTimer = null, this._hankoObserver = null, this._signUpHeadlines = /* @__PURE__ */ new Set([
4852
4852
  "Create an account",
4853
4853
  // en (our override)
4854
4854
  "Crear cuenta",
@@ -4875,9 +4875,9 @@ let oe = class extends qt {
4875
4875
  "Entrar"
4876
4876
  // pt loginEmailNoSignup
4877
4877
  ]), this._handleVisibilityChange = () => {
4878
- this._isPrimary && !document.hidden && !this.showProfile && !this.user && (this.log("Page visible, re-checking session..."), this.checkSession());
4878
+ this._isPrimary && (this._sessionCheckBackoffTimer || !document.hidden && !this.showProfile && !this.user && (this.log("Page visible, re-checking session..."), this.checkSession()));
4879
4879
  }, this._handleWindowFocus = () => {
4880
- this._isPrimary && !this.showProfile && !this.user && (this.log("Window focused, re-checking session..."), this.checkSession());
4880
+ this._isPrimary && (this._sessionCheckBackoffTimer || !this.showProfile && !this.user && (this.log("Window focused, re-checking session..."), this.checkSession()));
4881
4881
  }, this._handleExternalLogin = (n) => {
4882
4882
  var e;
4883
4883
  if (!this._isPrimary) return;
@@ -5040,6 +5040,13 @@ let oe = class extends qt {
5040
5040
  this.logError("Failed to initialize hanko-auth:", n), this.error = n.message, this.loading = !1, this._broadcastState();
5041
5041
  }
5042
5042
  }
5043
+ _scheduleSessionRetry() {
5044
+ if (this._sessionCheckBackoffTimer) return;
5045
+ const n = Math.min(1e3 * 2 ** this._sessionCheckFailures, 6e4);
5046
+ this.log(`Session check failed, retrying in ${n / 1e3}s (attempt ${this._sessionCheckFailures})`), this._sessionCheckBackoffTimer = setTimeout(() => {
5047
+ this._sessionCheckBackoffTimer = null, this.checkSession();
5048
+ }, n);
5049
+ }
5043
5050
  async checkSession() {
5044
5051
  var n, t, e, o, i;
5045
5052
  if (this.log("Checking for existing Hanko session..."), !this._hanko) {
@@ -5070,7 +5077,7 @@ let oe = class extends qt {
5070
5077
  ));
5071
5078
  return;
5072
5079
  }
5073
- this.log("Valid Hanko session found via cookie"), this.log("Session data:", s);
5080
+ this._sessionCheckFailures = 0, this.log("Valid Hanko session found via cookie"), this.log("Session data:", s);
5074
5081
  try {
5075
5082
  const d = await fetch(`${this.hankoUrl}/me`, {
5076
5083
  method: "GET",
@@ -5134,10 +5141,12 @@ let oe = class extends qt {
5134
5141
  } else
5135
5142
  this.log("No valid session cookie found - user needs to login");
5136
5143
  } catch (r) {
5137
- this.log("Session validation failed:", r), this.log("No valid session - user needs to login");
5144
+ this._sessionCheckFailures++, this.log("Session validation failed:", r), this._scheduleSessionRetry();
5145
+ return;
5138
5146
  }
5139
5147
  } catch (r) {
5140
- this.log("Session check error:", r), this.log("No existing session - user needs to login");
5148
+ this._sessionCheckFailures++, this.log("Session check error:", r), this._scheduleSessionRetry();
5149
+ return;
5141
5150
  } finally {
5142
5151
  this._isPrimary && this._broadcastState();
5143
5152
  }