where_is_waldo 0.1.13 → 0.1.15

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: d06a950b27e51d7506062dc38518912b7b631a91be886eb29801f01e0ac438ac
4
- data.tar.gz: fb1f82a4742a5099eee4d7a70f9d5445010dc52bd17408cfe2451363c04051a2
3
+ metadata.gz: 37761c093f1789ab27d7bc83b57b965deb0039a538e1dcef1450bb8680a5328d
4
+ data.tar.gz: 23fe780af170d951b2b961cc6674224030f1169247a328e1e37b98a16aceb1b3
5
5
  SHA512:
6
- metadata.gz: 5bfd8270b07bed9227be801b90b1f354a30a1eeaf748a3b7b7ff0d1a551bb0f3d6685792862f7c8e5676d411ea263b14fe64c67a76be422b0e55055a01672900
7
- data.tar.gz: '053823564618b09793ded991cf679d9017eb3be14c8069bf1d33ee0fbe50d8872b0e004a7d0ab8ac0969a08e3eb4c7b427c562800e2c002fa9ec8c1cc9bd3e22'
6
+ metadata.gz: 662057f2eb1431e5b25f74ee89f6fb387748d20526325f7391112b47ce1a6c4ed10d0039341c668eeeb5e78810ed5e4e956c3076673f76c8a0c9d9a57e59c7ff
7
+ data.tar.gz: 220b4eaf0a42eec12e38e960afae9c0c2dc150e83cd9b2a9cb4d97efd67da9265a49eb5b0f0e399c52105f019faedd0e322cc6dfdec6c839cd2581eef7b03fc0
data/CHANGELOG.md CHANGED
@@ -5,6 +5,71 @@ Notable changes to where_is_waldo. Format loosely follows
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.1.15 - 2026-11-10
9
+
10
+ ### Fixed
11
+
12
+ - **A reconnecting client kept sending its ORIGINAL token, forever.** The auth
13
+ token was read once and baked into the consumer's URL string at first
14
+ connect. ActionCable's `Consumer` declares `get url()`, which re-runs
15
+ `createWebSocketURL` on every connection attempt -- so passing a string meant
16
+ each reconnect after a sleep, a network blip or a server restart reused the
17
+ token from page load.
18
+
19
+ With a short-lived access token that is a silent, permanent failure. The
20
+ first reconnect after expiry is rejected, every retry sends the same dead
21
+ token, and the client sits disconnected while looking merely idle. Presence
22
+ empties and anything broadcast to "online" subscribers reaches nobody. In
23
+ one case a tab opened a socket and the server closed it 74ms later, rejected,
24
+ while the application kept working and reported nothing.
25
+
26
+ `getConsumer` now hands `createConsumer` a FUNCTION, so the token is resolved
27
+ per attempt and a refreshed one is picked up by the next retry with no
28
+ intervention.
29
+
30
+ ### Added
31
+
32
+ - **`getToken` may now return a promise.** A token provider that has to refresh
33
+ before it can answer could not be expressed before, because the URL callback
34
+ is invoked synchronously while opening a socket. An async provider is now
35
+ resolved in the background and its result cached for the next attempt, which
36
+ costs at most one refused attempt given ActionCable's backoff. Returning a
37
+ plain value still works exactly as before and stays synchronous.
38
+
39
+ - A throwing `getToken` no longer takes the socket down with it. A storage
40
+ accessor can raise outright with cookies blocked or in a private window; the
41
+ connection now proceeds without a token and lets the server decide.
42
+
43
+ ### Changed
44
+
45
+ - A **synchronous** `getToken` returning `null` is now taken at face value: the
46
+ socket connects with no token and the server decides. Previously the last
47
+ cached token was reused, so after a sign-out a client kept reconnecting with
48
+ the OLD token rather than being refused properly. A rejected or throwing
49
+ provider still falls back to the last known token, since those mean "could
50
+ not answer" rather than "there is no token".
51
+
52
+ ## 0.1.14 - 2026-11-06
53
+
54
+ ### Fixed
55
+
56
+ - **Cable connections were rejected in every app using an external identity
57
+ server.** `JwtConnection` resolved the connection's subject with
58
+ `find_by(id: payload["sub"])`. That is correct when `sub` is the application's
59
+ own user id, but an identity server issues a UUID subject, so the lookup
60
+ matched nothing and every connection was refused -- which meant no presence
61
+ and no broadcasting at all.
62
+
63
+ The failure was silent: a rejected connection is indistinguishable from a
64
+ client that never subscribed, and a count of presence rows cannot detect it
65
+ either, since those rows are TTL-transient and zero is also the healthy idle
66
+ state.
67
+
68
+ Resolution now goes through the host application's identity resolver when it
69
+ is in that mode, mapping the subject through `users.identity_id`. An
70
+ application whose presence subject is some other model keeps the
71
+ primary-key lookup, and an application without corebyscott is untouched.
72
+
8
73
  ## 0.1.13 - 2026-11-04
9
74
 
10
75
  ### Documentation
data/README.md CHANGED
@@ -81,6 +81,45 @@ configureCable({
81
81
  </PresenceProvider>
82
82
  ```
83
83
 
84
+ ### `getToken` and short-lived tokens
85
+
86
+ `getToken` is called **once per connection attempt**, not once at startup. That
87
+ matters because ActionCable reconnects on its own -- after a sleep, a network
88
+ blip, or a server restart -- and each attempt asks again. So returning the
89
+ current token keeps a long-lived tab connected, while capturing one in a
90
+ closure does not:
91
+
92
+ ```jsx
93
+ // Good: read at call time, so a rotated token is picked up on reconnect.
94
+ getToken: () => localStorage.getItem('token')
95
+
96
+ // Broken: captured once; every reconnect resends the original token, and once
97
+ // it expires the connection is refused for the life of the page.
98
+ const token = localStorage.getItem('token');
99
+ getToken: () => token
100
+ ```
101
+
102
+ **It may also return a promise**, for a provider that has to refresh before it
103
+ can answer:
104
+
105
+ ```jsx
106
+ getToken: async () => (await auth.freshToken()) || null
107
+ ```
108
+
109
+ The URL callback cannot await -- ActionCable invokes it synchronously while
110
+ opening a socket -- so an async provider is resolved in the background and its
111
+ result used from the next attempt. Given ActionCable's backoff that costs at
112
+ most one refused attempt. The provider is not invoked again while a resolve is
113
+ outstanding, so a reconnect storm will not fire a refresh request per attempt.
114
+
115
+ Two smaller guarantees:
116
+
117
+ - Returning `null` from a **synchronous** provider is taken at face value: the
118
+ socket connects with no token and the server decides. Use that on sign-out.
119
+ - A provider that **throws** does not take the connection down. The last known
120
+ token is used instead, since a storage accessor can raise outright with
121
+ cookies blocked or in a private window.
122
+
84
123
  ```jsx
85
124
  // any component — subscribe to an event type and filter the payload yourself.
86
125
  // Auto-unsubscribes on unmount; no central handler config.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.13
1
+ 0.1.15
@@ -50,13 +50,47 @@ module WhereIsWaldo
50
50
  payload = wiw_token_payload
51
51
  reject_unauthorized_connection unless payload
52
52
 
53
- klass = WhereIsWaldo.configuration.subject_class_constant
54
- user = klass&.find_by(id: payload["sub"] || payload[:sub])
53
+ user = wiw_resolve_subject(payload)
55
54
  reject_unauthorized_connection unless user
56
55
 
57
56
  user
58
57
  end
59
58
 
59
+ # `sub` is not always this app's primary key.
60
+ #
61
+ # Under corebyscott's :identity_server mode it is the identity server's
62
+ # UUID, so find_by(id:) matches nothing and EVERY cable connection is
63
+ # rejected -- silently, because a rejected connection is indistinguishable
64
+ # from a client that never subscribed. Core's resolver maps the UUID through
65
+ # users.identity_id, the same path its HTTP auth takes, so cable and request
66
+ # auth cannot disagree about who someone is.
67
+ #
68
+ # Only used when the configured subject IS core's user class. An app whose
69
+ # presence subject is some other model keeps the primary-key lookup, since
70
+ # core's resolver would hand back a user rather than that subject.
71
+ def wiw_resolve_subject(payload)
72
+ klass = WhereIsWaldo.configuration.subject_class_constant
73
+
74
+ if wiw_identity_mode? && klass && klass == wiw_core_user_class
75
+ return ::Corebyscott::IdentityResolver.resolve(payload)
76
+ end
77
+
78
+ klass&.find_by(id: payload["sub"] || payload[:sub])
79
+ end
80
+
81
+ def wiw_identity_mode?
82
+ defined?(::Corebyscott::IdentityResolver) &&
83
+ ::Corebyscott.respond_to?(:config) &&
84
+ ::Corebyscott.config.respond_to?(:auth_mode) &&
85
+ ::Corebyscott.config.auth_mode.to_s == "identity_server"
86
+ end
87
+
88
+ def wiw_core_user_class
89
+ return nil unless ::Corebyscott.config.respond_to?(:user_class_constant)
90
+
91
+ ::Corebyscott.config.user_class_constant
92
+ end
93
+
60
94
  # Optional container scoping from the JWT's `cid` claim. nil when the app
61
95
  # doesn't scope tokens to a container, or corebyscott isn't present.
62
96
  def wiw_container
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: where_is_waldo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.13
4
+ version: 0.1.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Gibson
@@ -79,6 +79,20 @@ dependencies:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: 7.2.1
82
+ - !ruby/object:Gem::Dependency
83
+ name: bundler-audit
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.9'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.9'
82
96
  - !ruby/object:Gem::Dependency
83
97
  name: redis
84
98
  requirement: !ruby/object:Gem::Requirement