umami-ruby 0.2.0 → 0.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8123faece9209e2ead6415c0e396a32853aa69ade5a11af2481c311a6e333e81
4
- data.tar.gz: ad58c5c87d2e27e92e90b7210a7443e589e4879ab9f9f247648b8c43f6af4a99
3
+ metadata.gz: 24515824a954d2c9b334b0445ef3a64f2faf79bb9fd0ec26657b702f0e170951
4
+ data.tar.gz: 2eec535b5aa02bf528b05a7d767d843b5781d8d970d03d7070953fc604582904
5
5
  SHA512:
6
- metadata.gz: 5cf0256f92097a0b2374205d727f1deb7f20d3dce31ade6783001023cd7512e76b3b4f65c41dc33166a340d4ae2e1dab60fef3f2043860fec9e3a98c1d1c7d3b
7
- data.tar.gz: 6f9905fc18ac2200c003d620d5f0f2caac972436b43c9b2756fee76547b7a92ced1358662d5a25300c1d41c3a42a2e847c4bca58d5eed727d57fc90eca1de236
6
+ metadata.gz: 53784a10bf0025c49791ace32354800ac39bdb7e662234a7f81f78f50538234aec5c32fda101bd7d844bd3df5eaf8a94e6002d85037729db637b8ff818e2d55f
7
+ data.tar.gz: 84cb4db13dd978dfb0ac65982081fe3a1a23b8e0b3f755c7cfc0c920e53c32d16861630b5a17c43059882c43222ce62f4e6aed1ff1b599d4da2e92bd56e56375
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [0.3.0] - 2026-07-24
2
+
3
+ ### Fixed
4
+
5
+ - **Username/password authentication against self-hosted instances actually works now.** Since the connection memoization was introduced, `authenticate` built (and memoized) the Faraday connection in order to POST `/api/auth/login` — before `@access_token` existed. Faraday bakes headers in at connection build time, so every request after a successful login went out without an `Authorization` header and failed with 401. The token-based flow was unaffected, which is how this survived: docs and tests exercised logins, never "login **then** call an endpoint". `authenticate` now drops the memoized connection after storing the token so the next request rebuilds it with the Bearer header, and the new `test/integration/authentication_flow_test.rb` pins the full lifecycle (login → authorized request → connection reuse without re-login).
6
+ - `authenticate` raises `Umami::AuthenticationError` when the login response is a 200 without a `token` field, instead of leaving a client that 401s on every call with no hint why.
7
+
8
+ ### Changed
9
+
10
+ - HTTP 401 and 403 responses now raise `Umami::AuthenticationError` instead of the undifferentiated `Umami::ClientError`, giving long-lived clients a re-authentication signal (expired/revoked token, insufficient permissions). To keep existing `rescue Umami::APIError` handlers working — they used to catch these as `ClientError` — `AuthenticationError` is now a subclass of `Umami::APIError` (previously it subclassed `Umami::Error` directly). Code matching `Umami::ClientError` specifically for 401/403 must switch to `Umami::AuthenticationError`.
11
+
1
12
  ## [0.2.0] - 2026-02-02
2
13
 
3
14
  ### Added
data/README.md CHANGED
@@ -440,12 +440,12 @@ begin
440
440
  client.website("non-existent-id")
441
441
  rescue Umami::NotFoundError => e
442
442
  puts "Website not found: #{e.message}"
443
+ rescue Umami::AuthenticationError => e
444
+ puts "Authentication failed: #{e.message}" # rejected login, expired/revoked token (401), forbidden (403)
443
445
  rescue Umami::ClientError => e
444
446
  puts "Client error: #{e.message}"
445
447
  rescue Umami::ServerError => e
446
448
  puts "Server error: #{e.message}"
447
- rescue Umami::AuthenticationError => e
448
- puts "Authentication failed: #{e.message}"
449
449
  rescue Umami::ConfigurationError => e
450
450
  puts "Configuration error: #{e.message}"
451
451
  rescue Umami::APIError => e
@@ -456,10 +456,10 @@ end
456
456
  **Error class hierarchy:**
457
457
  - `Umami::Error` - Base error class
458
458
  - `Umami::ConfigurationError` - Configuration issues
459
- - `Umami::AuthenticationError` - Authentication failures
460
459
  - `Umami::APIError` - API request failures
460
+ - `Umami::AuthenticationError` - Authentication failures: rejected logins, and HTTP 401/403 API responses (since 0.3.0 — an expired token on a long-lived client surfaces here so you can re-authenticate)
461
461
  - `Umami::NotFoundError` - Resource not found (404)
462
- - `Umami::ClientError` - Client errors (4xx)
462
+ - `Umami::ClientError` - Other client errors (4xx)
463
463
  - `Umami::ServerError` - Server errors (5xx)
464
464
 
465
465
  ## Logging
data/context7.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "url": "https://context7.com/rameerez/umami-ruby",
3
+ "public_key": "pk_HibNJE5rTFvy1txHHXUot"
4
+ }
data/lib/umami/client.rb CHANGED
@@ -72,6 +72,25 @@ module Umami
72
72
 
73
73
  data = JSON.parse(response.body)
74
74
  @access_token = data["token"]
75
+
76
+ # Umami answers a successful login with { "token": ..., "user": ... }
77
+ # (https://umami.is/docs/api/authentication#post-/api/auth/login).
78
+ # A 200 without a token would otherwise produce a client that fails
79
+ # every request with an opaque 401 — fail loudly at the source.
80
+ if @access_token.nil? || @access_token.empty?
81
+ raise Umami::AuthenticationError, "Authentication succeeded but the response carried no token"
82
+ end
83
+
84
+ # ⚠️ Load-bearing reset. The login POST above just memoized
85
+ # `@connection` — necessarily WITHOUT an Authorization header, since
86
+ # `@access_token` was still nil when the connection was built, and
87
+ # Faraday bakes headers into the connection at build time. Dropping
88
+ # the memo forces the next request to rebuild the connection, this
89
+ # time with the Bearer token (see #connection). Without this reset,
90
+ # every request after a username/password login fails with 401 —
91
+ # the exact bug shipped in <= 0.2.0 (why token-based auth was the
92
+ # only flow that worked against self-hosted instances).
93
+ @connection = nil
75
94
  rescue Faraday::Error, JSON::ParserError => e
76
95
  raise Umami::AuthenticationError, "Authentication failed: #{e.message}"
77
96
  end
@@ -1298,6 +1317,12 @@ module Umami
1298
1317
  handle_error(e)
1299
1318
  end
1300
1319
 
1320
+ # Memoized so keep-alive and middleware setup are paid once, BUT the
1321
+ # Authorization header is baked in at build time — Faraday connections
1322
+ # don't re-read `@access_token` afterwards. Anything that changes the
1323
+ # token MUST set `@connection = nil` so the next request rebuilds with
1324
+ # the fresh header (see #authenticate, where forgetting exactly that
1325
+ # was the <= 0.2.0 username/password bug).
1301
1326
  def connection
1302
1327
  @connection ||= Faraday.new(url: uri_base) do |faraday|
1303
1328
  faraday.request :json
@@ -1308,8 +1333,17 @@ module Umami
1308
1333
  end
1309
1334
  end
1310
1335
 
1336
+ # Order matters: Faraday::UnauthorizedError (401) and ForbiddenError
1337
+ # (403) are SUBCLASSES of Faraday::ClientError
1338
+ # (https://lostisland.github.io/faraday/#/middleware/included/raising-errors),
1339
+ # so they must be matched before the generic 4xx case or they'd
1340
+ # surface as an undifferentiated ClientError. Raising
1341
+ # AuthenticationError instead gives callers a re-authentication signal
1342
+ # (e.g. an expired or revoked token on a long-lived client).
1311
1343
  def handle_error(error)
1312
1344
  case error
1345
+ when Faraday::UnauthorizedError, Faraday::ForbiddenError
1346
+ raise Umami::AuthenticationError, "Authentication failed: #{error.message}"
1313
1347
  when Faraday::ResourceNotFound
1314
1348
  raise Umami::NotFoundError, "Resource not found: #{error.message}"
1315
1349
  when Faraday::ClientError
data/lib/umami/errors.rb CHANGED
@@ -5,12 +5,19 @@ module Umami
5
5
  # Error raised when there's a configuration issue
6
6
  class ConfigurationError < Error; end
7
7
 
8
- # Error raised when authentication fails
9
- class AuthenticationError < Error; end
10
-
11
8
  # Base error class for API-related errors
12
9
  class APIError < Error; end
13
10
 
11
+ # Error raised when authentication fails — a rejected login, a login
12
+ # response without a token, or an API request answered 401/403 (an
13
+ # expired or revoked token on a long-lived client, insufficient
14
+ # permissions). Deliberately a subclass of APIError since 0.2.1:
15
+ # before that, HTTP 401/403 surfaced as ClientError, so callers who
16
+ # `rescue Umami::APIError` were already catching them — subclassing
17
+ # APIError keeps those rescues working while giving auth failures
18
+ # their own class to match on.
19
+ class AuthenticationError < APIError; end
20
+
14
21
  # Error raised when a resource is not found
15
22
  class NotFoundError < APIError; end
16
23
 
data/lib/umami/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Umami
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: umami-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-02-03 00:00:00.000000000 Z
10
+ date: 2026-07-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: faraday
@@ -39,6 +39,7 @@ files:
39
39
  - LICENSE.txt
40
40
  - README.md
41
41
  - Rakefile
42
+ - context7.json
42
43
  - docs/Umami.html
43
44
  - docs/Umami/APIError.html
44
45
  - docs/Umami/AuthenticationError.html