ask-guests 0.1.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.
data/lib/ask/guests.rb ADDED
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "guests/version"
4
+ require_relative "guests/errors"
5
+ require_relative "guests/token"
6
+ require_relative "guests/session"
7
+ require_relative "guests/stores/base"
8
+ require_relative "guests/stores/memory"
9
+ require_relative "guests/budget"
10
+ require_relative "guests/budget/policy"
11
+ require_relative "guests/budget/unlimited"
12
+ require_relative "guests/budget/turn_limit"
13
+ require_relative "guests/budget/token_quota"
14
+ require_relative "guests/claim"
15
+ require_relative "guests/sweep"
16
+
17
+ module Ask
18
+ # Anonymous, cookie-backed guest access with metered AI usage.
19
+ #
20
+ # A guest session is a durable identity for a visitor who has not signed
21
+ # up: a random id behind an HMAC-signed token, plus counters used to meter
22
+ # agent usage and a single-use claim that transfers everything the guest
23
+ # owns to a real owner when they authenticate.
24
+ #
25
+ # The core is framework-agnostic — plain Ruby with stdlib only. Optional
26
+ # adapters plug in persistence and web frameworks:
27
+ #
28
+ # require "ask-guests/active_record" # ActiveRecord store + model base
29
+ # require "ask-guests/rails" # controller concern, Devise hook, job
30
+ #
31
+ # @example Configuration
32
+ # Ask::Guests.configure do |config|
33
+ # config.secret = ENV.fetch("GUEST_TOKEN_SECRET")
34
+ # config.store = Ask::Guests::Stores::Memory.new
35
+ # config.policies = [Ask::Guests::Budget::TurnLimit.new(per_day: 40)]
36
+ # config.retention = 14 * 24 * 60 * 60 # seconds
37
+ # end
38
+ #
39
+ # @example Metering an agent turn
40
+ # guest = Ask::Guests.store.find(id) # or current_guest_session in Rails
41
+ # budget = Ask::Guests.budget
42
+ #
43
+ # unless budget.allowed?(guest)
44
+ # # show "sign up to keep going"
45
+ # end
46
+ # response = agent.run(...)
47
+ # budget.record!(guest, turns: 1, tokens: response.total_tokens)
48
+ #
49
+ # @example Claiming on sign-up
50
+ # Ask::Guests.claimable(:work_requests) do |session, owner|
51
+ # WorkRequest.where(guest_session_id: session.id)
52
+ # .update_all(account_id: owner.account_id, account_user_id: owner.id, guest_session_id: nil)
53
+ # end
54
+ #
55
+ # Ask::Guests::Claim.new(session: guest, owner: account_user).call # => true/false
56
+ module Guests
57
+ class << self
58
+ def configure
59
+ yield configuration
60
+ end
61
+
62
+ def configuration
63
+ @configuration ||= Configuration.new
64
+ end
65
+
66
+ def reset_configuration!
67
+ @configuration = nil
68
+ end
69
+
70
+ def secret
71
+ configuration.secret
72
+ end
73
+
74
+ def store
75
+ configuration.store
76
+ end
77
+
78
+ # A budget built from the current configuration. Rebuilt on each call
79
+ # so configuration changes (and test resets) always take effect.
80
+ def budget
81
+ Budget.new(
82
+ policies: configuration.policies,
83
+ store: configuration.store,
84
+ clock: configuration.clock
85
+ )
86
+ end
87
+
88
+ # Registered record-transfer handlers, run by {Guests::Claim}.
89
+ # @return [Hash{String => Proc}]
90
+ def claimables
91
+ @claimables ||= {}
92
+ end
93
+
94
+ # Register a handler that transfers records the guest session owns to
95
+ # a real owner. Called inside the claim's transaction.
96
+ #
97
+ # Ask::Guests.claimable(:work_requests) { |session, owner| ... }
98
+ def claimable(name, &block)
99
+ raise ArgumentError, "claimable requires a block" unless block
100
+
101
+ claimables[name.to_s] = block
102
+ end
103
+
104
+ def clear_claimables!
105
+ @claimables = {}
106
+ end
107
+
108
+ def sweep
109
+ Sweep.new(
110
+ store: configuration.store,
111
+ retention: configuration.retention,
112
+ clock: configuration.clock
113
+ )
114
+ end
115
+ end
116
+
117
+ # Configuration for the guest system. All values have working defaults
118
+ # except +secret+, which is required to sign and verify tokens.
119
+ class Configuration
120
+ # HMAC secret for signing guest tokens (required).
121
+ attr_accessor :secret
122
+ # Persistence adapter (see Stores::Base). Defaults to in-memory.
123
+ attr_accessor :store
124
+ # Usage policies (see Budget::TurnLimit, Budget::TokenQuota).
125
+ attr_accessor :policies
126
+ # Unclaimed sessions older than this many seconds are swept.
127
+ attr_accessor :retention
128
+ # Injectable clock returning a Time (used for daily counter rollover
129
+ # and sweep cutoffs).
130
+ attr_accessor :clock
131
+ # Name of the cookie holding the signed guest token (Rails adapter).
132
+ attr_accessor :cookie_name
133
+ # Flash message shown after a successful claim (Rails adapter).
134
+ attr_accessor :claim_notice
135
+ # Minimum seconds between last-seen writes while a guest browses
136
+ # (Rails adapter). Keeps ordinary page views from writing per request.
137
+ attr_accessor :touch_interval
138
+ # Queue for the Rails sweep job.
139
+ attr_accessor :sweep_queue
140
+
141
+ def initialize
142
+ @secret = nil
143
+ @store = Stores::Memory.new
144
+ @policies = [Budget::Unlimited.new]
145
+ @retention = 14 * 24 * 60 * 60
146
+ @clock = -> { Time.now.utc }
147
+ @cookie_name = "guest_session_token"
148
+ @claim_notice = "Welcome! We've moved your work to your account."
149
+ @touch_interval = 300
150
+ @sweep_queue = :default
151
+ end
152
+
153
+ def now
154
+ clock.call
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ask-guests"
4
+ require "active_record"
5
+
6
+ require_relative "../ask/guests/active_record/json_value"
7
+ require_relative "../ask/guests/active_record/session_base"
8
+ require_relative "../ask/guests/active_record/session_store"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ask-guests"
4
+ require "active_support/concern"
5
+ require "active_job"
6
+
7
+ require_relative "../ask/guests/rails/controller"
8
+ require_relative "../ask/guests/rails/claim_on_authentication"
9
+ require_relative "../ask/guests/rails/sweep_job"
data/lib/ask-guests.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ask/guests"
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Ask
6
+ module Guests
7
+ module Generators
8
+ # bin/rails generate ask:guests:install
9
+ class InstallGenerator < ::Rails::Generators::Base
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ desc "Creates the guest_sessions migration and ask-guests initializer"
13
+
14
+ def create_initializer
15
+ template "initializer.rb", "config/initializers/ask_guests.rb"
16
+ end
17
+
18
+ def create_migration
19
+ timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
20
+ template "migration.rb", "db/migrate/#{timestamp}_create_guest_sessions.rb"
21
+ end
22
+
23
+ def show_readme
24
+ say <<~MESSAGE
25
+
26
+ ask-guests installed.
27
+
28
+ Next steps:
29
+
30
+ 1. Define the model (app/models/guest_session.rb):
31
+
32
+ class GuestSession < Ask::Guests::ActiveRecord::SessionBase
33
+ self.table_name = "guest_sessions"
34
+
35
+ # Destroying a session sweeps everything the guest owned:
36
+ # has_many :work_requests, foreign_key: :guest_session_id, dependent: :destroy
37
+ end
38
+
39
+ 2. Register what claiming transfers (config/initializers/ask_guests.rb).
40
+
41
+ 3. Allow guests in a controller:
42
+
43
+ include Ask::Guests::Rails::Controller
44
+ allow_guest_access
45
+ before_action :require_visitor
46
+
47
+ 4. Claim on authentication (Devise example):
48
+
49
+ include Ask::Guests::Rails::ClaimOnAuthentication
50
+ after_action :claim_guest_session, only: :create
51
+
52
+ def guest_claim_owner = current_user.account_users.first
53
+
54
+ 5. Schedule the sweep:
55
+
56
+ production:
57
+ guests_sweep:
58
+ class: "Ask::Guests::Rails::SweepJob"
59
+ schedule: every day at 4am
60
+
61
+ MESSAGE
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Guest access configuration (ask-guests).
4
+ #
5
+ # Anonymous visitors get a signed-cookie session, metered agent usage, and
6
+ # a single-use claim that moves everything they own to a real account when
7
+ # they sign up. See https://github.com/ask-rb/ask-guests for the full guide.
8
+
9
+ Ask::Guests.configure do |config|
10
+ # Required: HMAC secret for guest tokens. Rotating it invalidates all
11
+ # outstanding guest cookies (they simply start fresh).
12
+ config.secret = Rails.application.secret_key_base
13
+
14
+ # Persistence. Define the model first (see the install generator output):
15
+ #
16
+ # class GuestSession < Ask::Guests::ActiveRecord::SessionBase
17
+ # self.table_name = "guest_sessions"
18
+ # has_many :work_requests, foreign_key: :guest_session_id, dependent: :destroy
19
+ # end
20
+ config.store = Ask::Guests::ActiveRecord::SessionStore.new(model: GuestSession)
21
+
22
+ # Usage caps for anonymous visitors. Signing up lifts them because a
23
+ # signed-in request never touches the guest budget.
24
+ config.policies = [
25
+ Ask::Guests::Budget::TurnLimit.new(per_day: 40)
26
+ # Ask::Guests::Budget::TokenQuota.new(per_day: 10_000)
27
+ ]
28
+
29
+ # Unclaimed sessions inactive for this long are swept (seconds).
30
+ config.retention = 14 * 24 * 60 * 60
31
+
32
+ # What claiming transfers. Register one handler per owned model; they run
33
+ # inside the claim's transaction, exactly once.
34
+ #
35
+ # Ask::Guests.claimable(:work_requests) do |session, owner|
36
+ # WorkRequest.where(guest_session_id: session.id)
37
+ # .update_all(account_id: owner.account_id, account_user_id: owner.id, guest_session_id: nil)
38
+ # end
39
+ end
@@ -0,0 +1,16 @@
1
+ class CreateGuestSessions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :guest_sessions do |t|
4
+ t.jsonb :counters, null: false, default: {}
5
+ t.jsonb :metadata, null: false, default: {}
6
+ t.datetime :last_seen_at
7
+ t.datetime :converted_at
8
+ t.references :owner, polymorphic: true
9
+ t.timestamps
10
+ end
11
+
12
+ # Speculative: sessions at risk of being swept.
13
+ add_index :guest_sessions, :converted_at
14
+ add_index :guest_sessions, :last_seen_at
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,159 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ask-guests
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kaka Ruto
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.25'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.25'
26
+ - !ruby/object:Gem::Dependency
27
+ name: mocha
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.1'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: activerecord
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '7.1'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '7.1'
68
+ - !ruby/object:Gem::Dependency
69
+ name: sqlite3
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '1.4'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '1.4'
82
+ - !ruby/object:Gem::Dependency
83
+ name: activejob
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '7.1'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '7.1'
96
+ description: 'Give anonymous visitors a durable identity with metered AI access: signed
97
+ guest sessions, per-day turn/token budgets, single-use claiming of records into
98
+ a real owner on sign-up, and stale-session retention. Framework-agnostic core with
99
+ optional ActiveRecord and Rails adapters.'
100
+ email:
101
+ - kaka@myrrlabs.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - CHANGELOG.md
107
+ - LICENSE
108
+ - README.md
109
+ - lib/ask-guests.rb
110
+ - lib/ask-guests/active_record.rb
111
+ - lib/ask-guests/rails.rb
112
+ - lib/ask/guests.rb
113
+ - lib/ask/guests/active_record/json_value.rb
114
+ - lib/ask/guests/active_record/session_base.rb
115
+ - lib/ask/guests/active_record/session_store.rb
116
+ - lib/ask/guests/budget.rb
117
+ - lib/ask/guests/budget/policy.rb
118
+ - lib/ask/guests/budget/token_quota.rb
119
+ - lib/ask/guests/budget/turn_limit.rb
120
+ - lib/ask/guests/budget/unlimited.rb
121
+ - lib/ask/guests/claim.rb
122
+ - lib/ask/guests/errors.rb
123
+ - lib/ask/guests/rails/claim_on_authentication.rb
124
+ - lib/ask/guests/rails/controller.rb
125
+ - lib/ask/guests/rails/sweep_job.rb
126
+ - lib/ask/guests/session.rb
127
+ - lib/ask/guests/stores/base.rb
128
+ - lib/ask/guests/stores/memory.rb
129
+ - lib/ask/guests/sweep.rb
130
+ - lib/ask/guests/token.rb
131
+ - lib/ask/guests/version.rb
132
+ - lib/generators/ask/guests/install/install_generator.rb
133
+ - lib/generators/ask/guests/install/templates/initializer.rb
134
+ - lib/generators/ask/guests/install/templates/migration.rb
135
+ homepage: https://github.com/ask-rb/ask-guests
136
+ licenses:
137
+ - MIT
138
+ metadata:
139
+ homepage_uri: https://github.com/ask-rb/ask-guests
140
+ source_code_uri: https://github.com/ask-rb/ask-guests
141
+ changelog_uri: https://github.com/ask-rb/ask-guests/blob/master/CHANGELOG.md
142
+ rdoc_options: []
143
+ require_paths:
144
+ - lib
145
+ required_ruby_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '3.2'
150
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ requirements: []
156
+ rubygems_version: 4.0.18
157
+ specification_version: 4
158
+ summary: Anonymous, cookie-backed guest access for ask-rb apps
159
+ test_files: []