wefunder 0.1.0.beta2 → 0.1.0.beta3

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: 946ede1e59f03277730fa442f9df079e44bac9532db1587306f488d1d8c0a616
4
- data.tar.gz: 53ca111474c48a0e954a690caa7001b34cad1c28cd9ba2e275d2489e5f3a235c
3
+ metadata.gz: a3ac10a62a698ee6cb163f3ec6e100248d461dc6c55bf97caeb1bac349b44af6
4
+ data.tar.gz: 26d0e08433a88d288942da627f8d509ffa196e6e2ed3cc347bae43beadaf84bf
5
5
  SHA512:
6
- metadata.gz: 96cafc3351d195aae306c22a512d0ae1ea7613b2a0d7ffdc843cfc2c37614006effae50197c38c175020ecee6145140e2271f5c26355b39a53eac7a2636626c4
7
- data.tar.gz: 381a50ca34d2084e923b4336e4d39b7d6403ad6f2c15feb03602493d0ab1ad3ebf88123bf5bf86cb75bab1a718f7709f00e4b7d8d79b1ac34973e7d22fe1ef3e
6
+ metadata.gz: 4ee97ef8052e3e7a9476a8b0243b52a706bfd2c6a5911e1b9362c07e5d5d6290529615ed6daace3d953769d126a492440692ebef5191cd7846c418a70f6a54b5
7
+ data.tar.gz: 535800baceafa25b8fcb744d2a1fc16a0596e14d80db5fba6f0c83772ee371c16a361335f82819b5a5e664bf016d17b008d3ff46a3f07f527eab491d8aa36bdd
data/README.md CHANGED
@@ -84,10 +84,11 @@ class TokenStore
84
84
  end
85
85
  ```
86
86
 
87
- The rotated set is saved **before** any thread can use it. If `store.save` raises, the SDK still
88
- switches to the new set in memory (the old refresh token is already dead) and raises
89
- `Wefunder::TokenPersistenceError`, whose `tokens` is the set that is live but not durable. Alert on it
90
- and retry the save; do not swallow it, or a restart will not be able to reconnect.
87
+ The rotated set is saved **before** any thread can use it. If `store.save` or `on_token_refresh` raises, the SDK raises
88
+ `Wefunder::TokenPersistenceError` and keeps the rotated set *pending*: no request is made with it, the
89
+ consumed refresh token is never reused, and the next call retries the save (or persist
90
+ `error.tokens` yourself and call `wf.token_manager.mark_persisted!(error.tokens)`; a stale acknowledgment after another rotation is a no-op). Alert on this error; a process
91
+ that keeps failing to save cannot reconnect after a restart.
91
92
 
92
93
  Concurrent requests that hit a 401 at the same time share one refresh (the client is thread-safe).
93
94
  If several application instances can use the same OAuth connection, serialize refreshes for that
@@ -229,10 +230,13 @@ sends a fully-wrapped request and returns the decoded JSON.
229
230
  ```bash
230
231
  bundle install
231
232
  bundle exec rubocop
232
- bundle exec rspec # hermetic; spec/e2e is excluded by rake and CI on PRs
233
- bundle exec rspec spec/e2e # live sandbox, with WEFUNDER_CLIENT_ID / WEFUNDER_CLIENT_SECRET
233
+ bundle exec rspec # hermetic — live examples are tagged :e2e and filtered out
234
+ WEFUNDER_E2E=1 bundle exec rspec spec/e2e # live sandbox; also needs WEFUNDER_CLIENT_ID / WEFUNDER_CLIENT_SECRET
234
235
  ```
235
236
 
237
+ The live group is opt-in by tag, not by path: without `WEFUNDER_E2E=1` it never runs, even when sandbox
238
+ credentials happen to be exported in your shell.
239
+
236
240
  Generated files in `lib/wefunder_generated/` come from `openapi/openapi.yaml` via `script/generate`
237
241
  (openapi-generator in Docker; no Java needed) and are never edited by hand.
238
242
 
@@ -70,6 +70,9 @@ module Wefunder
70
70
  # The current token set (rotated refresh token included).
71
71
  def tokens = @token_manager.current
72
72
 
73
+ # The TokenManager (for +mark_persisted!+ after handling a TokenPersistenceError).
74
+ attr_reader :token_manager
75
+
73
76
  # The generated APIs, one accessor per tag (+wf.raw.syndicate_members.list_syndicate_members(id)+).
74
77
  # Wrap calls in +wrap+ to get typed errors.
75
78
  attr_reader :raw
@@ -8,8 +8,10 @@ module Wefunder
8
8
  # manager already holds a different one, another caller recovered in the meantime and the
9
9
  # current set is returned without a round-trip. The rotated set is persisted (store +
10
10
  # callback) BEFORE the retried request can use it. Thread-safe (Mutex).
11
- # Raised when the token store failed to save a rotated token set. +tokens+ is the set that
12
- # is now live in memory but NOT durable; +cause+ is the store's exception.
11
+ # Raised when the token store failed to save a rotated token set. +tokens+ is the rotated
12
+ # set that is NOT yet durable and NOT yet in use; the manager keeps it pending and retries
13
+ # the save on the next call (or persist it yourself and call +mark_persisted!(tokens)+). Until it is
14
+ # saved no request is made with it — the consumed refresh token is never reused either.
13
15
  class TokenPersistenceError < StandardError
14
16
  attr_reader :tokens
15
17
 
@@ -22,7 +24,9 @@ module Wefunder
22
24
  class TokenManager
23
25
  DEFAULT_EXPIRY_LEEWAY_SECONDS = 30.0
24
26
 
25
- attr_reader :current
27
+ # The durable, in-use token set. A rotated set that could not be persisted is held in
28
+ # +pending_tokens+ instead until a save succeeds.
29
+ attr_reader :current, :pending_tokens
26
30
 
27
31
  def initialize(tokens, client_id: nil, client_secret: nil, re_mint: nil, on_token_refresh: nil, store: nil,
28
32
  faraday: nil, now: nil, expiry_leeway_seconds: DEFAULT_EXPIRY_LEEWAY_SECONDS,
@@ -46,16 +50,35 @@ module Wefunder
46
50
  can_rotate? || !@re_mint.nil?
47
51
  end
48
52
 
49
- # A valid access token, refreshing proactively if expired / within the leeway.
53
+ # A valid access token, refreshing proactively if expired / within the leeway. If a rotated
54
+ # set is pending persistence, the save is retried first — no request uses an undurable token.
50
55
  def access_token
56
+ @lock.synchronize { publish_pending! if @pending_tokens } if @pending_tokens # re-check under the lock
51
57
  refresh(stale_token: @current.access_token) if near_expiry? && can_refresh?
52
58
  @current.access_token
53
59
  end
54
60
 
61
+ # Tell the manager you persisted +tokens+ (the set from a TokenPersistenceError) yourself.
62
+ # Publishes it only if it is still the pending set; a stale acknowledgment (the manager has
63
+ # since rotated again) is a no-op and returns false, so an older save can never publish a
64
+ # newer, unsaved set.
65
+ def mark_persisted!(tokens)
66
+ @lock.synchronize do
67
+ return false unless @pending_tokens && same_token_set?(@pending_tokens, tokens)
68
+
69
+ @current = @pending_tokens
70
+ @pending_tokens = nil
71
+ true
72
+ end
73
+ end
74
+
55
75
  # Recover the token (after a 401 or proactively). If +stale_token+ is no longer the current
56
76
  # token, someone else already recovered and the current set is returned.
57
77
  def refresh(stale_token: nil)
58
78
  @lock.synchronize do
79
+ # A rotated set awaiting persistence: retry the save rather than rotating again (the
80
+ # old refresh token was consumed by that rotation).
81
+ return publish_pending! if @pending_tokens
59
82
  return @current if stale_token && @current.access_token != stale_token
60
83
 
61
84
  previous_refresh = @current.refresh_token
@@ -71,26 +94,35 @@ module Wefunder
71
94
  else
72
95
  raise AuthError, "Access token expired and no refresh token / re-mint capability is configured."
73
96
  end
74
- persist(nxt)
75
- @current = nxt
76
- @on_token_refresh&.call(nxt)
77
- nxt
97
+ @pending_tokens = nxt
98
+ publish_pending!
78
99
  end
79
100
  end
80
101
 
81
- # Persist BEFORE publishing: no other thread may use the rotated token until it is durable,
82
- # and a failed save must not leave the process working in memory but unable to reconnect
83
- # after a restart. If the store raises we still have the only copy of the rotated refresh
84
- # token (the old one is dead), so it is published anyway and the failure is surfaced as
85
- # TokenPersistenceError — handle it (alert, retry the save with +error.tokens+), don't swallow it.
86
- def persist(tokens)
87
- @store&.save(tokens)
88
- rescue StandardError => e
102
+ private
103
+
104
+ # Persist BEFORE publishing (caller holds the lock): no thread may use the rotated token
105
+ # until it is durable, and a failed save must not leave the process working in memory but
106
+ # unable to reconnect after a restart. On failure the set stays in +pending_tokens+ and
107
+ # TokenPersistenceError is raised; the next call retries the save.
108
+ def publish_pending!
109
+ tokens = @pending_tokens
110
+ begin
111
+ @store&.save(tokens)
112
+ # on_token_refresh is a persistence path too, so it runs BEFORE publication and a failure
113
+ # keeps the set pending exactly like a store failure.
114
+ @on_token_refresh&.call(tokens)
115
+ rescue StandardError => e
116
+ raise TokenPersistenceError.new(tokens, e)
117
+ end
118
+ @pending_tokens = nil
89
119
  @current = tokens
90
- raise TokenPersistenceError.new(tokens, e)
120
+ tokens
91
121
  end
92
122
 
93
- private
123
+ def same_token_set?(a, b)
124
+ a.access_token == b.access_token && a.refresh_token == b.refresh_token
125
+ end
94
126
 
95
127
  def can_rotate?
96
128
  !@current.refresh_token.nil? && !@current.refresh_token.empty? && !@client_id.nil?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wefunder
4
- VERSION = "0.1.0.beta2"
4
+ VERSION = "0.1.0.beta3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wefunder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.beta2
4
+ version: 0.1.0.beta3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wefunder