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 +4 -4
- data/CHANGELOG.md +65 -0
- data/README.md +39 -0
- data/VERSION +1 -1
- data/app/channels/where_is_waldo/jwt_connection.rb +36 -2
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 37761c093f1789ab27d7bc83b57b965deb0039a538e1dcef1450bb8680a5328d
|
|
4
|
+
data.tar.gz: 23fe780af170d951b2b961cc6674224030f1169247a328e1e37b98a16aceb1b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|