syntropy 0.32.0 → 0.33.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.
@@ -1,28 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'helper'
4
3
  require 'base64'
5
4
  require 'digest'
6
5
 
7
- class OAuthBaseTest < Minitest::Test
8
- APP_ROOT = File.expand_path(File.join(__dir__, '../app'))
9
- HTTP = Syntropy::HTTP
10
-
6
+ class OAuthBaseTest < Syntropy::Test
11
7
  def setup
12
- @machine = UM.new
13
- @app = Syntropy::App.new(
14
- root_dir: APP_ROOT,
15
- mount_path: '/',
16
- machine: @machine
17
- )
18
- @store = @app.module_loader.load('_lib/auth_store')
19
- @test_harness = Syntropy::TestHarness.new(@app)
20
- end
21
-
22
- def teardown
23
- @machine = nil
24
- @app = nil
25
- @test_harness = nil
8
+ super
9
+ @store = load_module('_lib/auth_store')
26
10
  end
27
11
  end
28
12
 
@@ -42,18 +26,15 @@ end
42
26
 
43
27
  class OAuthPhase1DiscoveryTest < OAuthBaseTest
44
28
  def test_mcp_no_bearer_token
45
- req = @test_harness.request(
29
+ req = post_json(
30
+ '/mcp',
46
31
  {
47
- ':method' => 'POST',
48
- ':path' => '/mcp',
49
- 'content-type' => 'application/json'
50
- },
51
- JSON.dump({
52
32
  method: 'initialize',
53
33
  jsonrpc: '2.0',
54
34
  params: {}
55
- })
35
+ }
56
36
  )
37
+
57
38
  assert_equal HTTP::UNAUTHORIZED, req.response_status
58
39
 
59
40
  www_auth = req.response_headers['WWW-Authenticate']
@@ -62,18 +43,14 @@ class OAuthPhase1DiscoveryTest < OAuthBaseTest
62
43
  end
63
44
 
64
45
  def test_mcp_invalid_bearer_token
65
- req = @test_harness.request(
46
+ req = post_json(
47
+ '/mcp',
66
48
  {
67
- ':method' => 'POST',
68
- ':path' => '/mcp',
69
- 'content-type' => 'application/json',
70
- 'authorization' => 'Bearer foobar'
71
- },
72
- JSON.dump({
73
49
  method: 'initialize',
74
50
  jsonrpc: '2.0',
75
51
  params: {}
76
- })
52
+ },
53
+ 'authorization' => 'Bearer foobar'
77
54
  )
78
55
  assert_equal HTTP::UNAUTHORIZED, req.response_status
79
56
 
@@ -83,10 +60,7 @@ class OAuthPhase1DiscoveryTest < OAuthBaseTest
83
60
  end
84
61
 
85
62
  def test_oauth_protected_resource_metadatas
86
- req = @test_harness.request(
87
- ':method' => 'GET',
88
- ':path' => '/.well-known/oauth-protected-resource'
89
- )
63
+ req = get('/.well-known/oauth-protected-resource')
90
64
  assert_equal HTTP::OK, req.response_status
91
65
  json = req.response_json
92
66
  assert_equal ["http://localhost:1234/"], json['authorization_servers']
@@ -94,10 +68,7 @@ class OAuthPhase1DiscoveryTest < OAuthBaseTest
94
68
  end
95
69
 
96
70
  def test_oauth_authorization_server_metadata
97
- req = @test_harness.request(
98
- ':method' => 'GET',
99
- ':path' => '/.well-known/oauth-authorization-server'
100
- )
71
+ req = get('/.well-known/oauth-authorization-server')
101
72
  assert_equal HTTP::OK, req.response_status
102
73
  json = req.response_json
103
74
  assert_equal "http://localhost:1234/", json['issuer']
@@ -117,14 +88,10 @@ class OAuthPhase2ClientRegistrationTest < OAuthBaseTest
117
88
  "grant_types" => ["authorization_code", "refresh_token"]
118
89
  }
119
90
 
120
- req = @test_harness.request(
121
- {
122
- ':method' => 'POST',
123
- ':path' => '/oauth/register',
124
- 'content-type' => 'application/json',
125
- 'authorization' => 'Bearer foobar'
126
- },
127
- JSON.dump(client_info)
91
+ req = post_json(
92
+ '/oauth/register',
93
+ client_info,
94
+ 'authorization' => 'Bearer foobar'
128
95
  )
129
96
 
130
97
  assert_equal HTTP::CREATED, req.response_status
@@ -148,14 +115,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
148
115
  "redirect_uris" => ["http://localhost:8400/callback"],
149
116
  "grant_types" => ["authorization_code", "refresh_token"]
150
117
  }
151
- req = @test_harness.request(
152
- {
153
- ':method' => 'POST',
154
- ':path' => '/oauth/register',
155
- 'content-type' => 'application/json',
156
- 'authorization' => 'Bearer foobar'
157
- },
158
- JSON.dump(client_info)
118
+ req = post_json(
119
+ '/oauth/register',
120
+ client_info,
121
+ 'authorization' => 'Bearer foobar'
159
122
  )
160
123
  assert_equal HTTP::CREATED, req.response_status
161
124
  json = req.response_json
@@ -169,10 +132,7 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
169
132
  'code_challenge_method' => 'S256',
170
133
  'state' => 'my_state'
171
134
  }
172
- req = @test_harness.request(
173
- ':method' => 'GET',
174
- ':path' => "/oauth/authorize?#{URI.encode_www_form(params)}"
175
- )
135
+ req = get("/oauth/authorize?#{URI.encode_www_form(params)}")
176
136
  assert_equal HTTP::FOUND, req.response_status
177
137
  assert_equal '/signin', req.response_headers['Location']
178
138
 
@@ -202,17 +162,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
202
162
  }
203
163
  oauth_signin_id = @store.store(auth_params)
204
164
 
205
- req = @test_harness.request(
206
- {
207
- ':method' => 'POST',
208
- ':path' => '/signin',
209
- 'cookie' => "oauth_signin_id=#{oauth_signin_id}",
210
- 'content-type' => 'application/x-www-form-urlencoded'
211
- },
212
- URI.encode_www_form(
213
- username: 'foobar',
214
- password: 'foobar'
215
- )
165
+ req = post_form(
166
+ '/signin',
167
+ { username: 'foobar', password: 'foobar' },
168
+ 'cookie' => "oauth_signin_id=#{oauth_signin_id}",
216
169
  )
217
170
 
218
171
  auth_info = @store.fetch(oauth_signin_id)
@@ -227,27 +180,15 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
227
180
  end
228
181
 
229
182
  def test_signin_endpoint_get
230
- req = @test_harness.request(
231
- {
232
- ':method' => 'GET',
233
- ':path' => '/signin',
234
- }
235
- )
183
+ req = get('/signin')
236
184
  assert_equal HTTP::OK, req.response_status
237
185
  assert_equal 'text/html', req.response_content_type
238
186
  end
239
187
 
240
188
  def test_signin_endpoint_post_bad_creds
241
- req = @test_harness.request(
242
- {
243
- ':method' => 'POST',
244
- ':path' => '/signin',
245
- 'content-type' => 'application/x-www-form-urlencoded'
246
- },
247
- URI.encode_www_form(
248
- username: 'foobar',
249
- password: 'bad'
250
- )
189
+ req = post_form(
190
+ '/signin',
191
+ { username: 'foobar', password: 'bad' }
251
192
  )
252
193
  assert_equal HTTP::UNAUTHORIZED, req.response_status
253
194
  assert_equal 'text/html', req.response_content_type
@@ -255,16 +196,9 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
255
196
  end
256
197
 
257
198
  def test_signin_endpoint_post_good_creds
258
- req = @test_harness.request(
259
- {
260
- ':method' => 'POST',
261
- ':path' => '/signin',
262
- 'content-type' => 'application/x-www-form-urlencoded'
263
- },
264
- URI.encode_www_form(
265
- username: 'foobar',
266
- password: 'foobar'
267
- )
199
+ req = post_form(
200
+ '/signin',
201
+ { username: 'foobar', password: 'foobar' }
268
202
  )
269
203
  assert_equal HTTP::SEE_OTHER, req.response_status
270
204
  assert_equal '/', req.response_headers['Location']
@@ -277,22 +211,14 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
277
211
  end
278
212
 
279
213
  def test_oauth_consent_endpoint_get_no_oauth_signin_id
280
- req = @test_harness.request(
281
- {
282
- ':method' => 'GET',
283
- ':path' => '/oauth/consent',
284
- }
285
- )
214
+ req = get('/oauth/consent')
286
215
  assert_equal HTTP::BAD_REQUEST, req.response_status
287
216
  end
288
217
 
289
218
  def test_oauth_consent_endpoint_get_invalid_oauth_signin_id
290
- req = @test_harness.request(
291
- {
292
- ':method' => 'GET',
293
- ':path' => '/oauth/consent',
294
- 'cookie' => 'outh_signin_id=foo'
295
- }
219
+ req = get(
220
+ '/oauth/consent',
221
+ 'cookie' => 'outh_signin_id=foo'
296
222
  )
297
223
  assert_equal HTTP::BAD_REQUEST, req.response_status
298
224
  end
@@ -321,12 +247,9 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
321
247
  }
322
248
  oauth_signin_id = @store.store(auth_params)
323
249
 
324
- req = @test_harness.request(
325
- {
326
- ':method' => 'GET',
327
- ':path' => '/oauth/consent',
328
- 'cookie' => "oauth_signin_id=#{oauth_signin_id}"
329
- }
250
+ req = get(
251
+ '/oauth/consent',
252
+ 'cookie' => "oauth_signin_id=#{oauth_signin_id}"
330
253
  )
331
254
  assert_equal HTTP::OK, req.response_status
332
255
  end
@@ -355,16 +278,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
355
278
  }
356
279
  oauth_signin_id = @store.store(auth_params)
357
280
 
358
- req = @test_harness.request(
359
- {
360
- ':method' => 'POST',
361
- ':path' => '/oauth/consent',
362
- 'cookie' => "oauth_signin_id=#{oauth_signin_id}",
363
- 'content-type' => 'application/x-www-form-urlencoded'
364
- },
365
- URI.encode_www_form(
366
- decision: 'deny',
367
- )
281
+ req = post_form(
282
+ '/oauth/consent',
283
+ { decision: 'deny' },
284
+ 'cookie' => "oauth_signin_id=#{oauth_signin_id}"
368
285
  )
369
286
  assert_equal HTTP::FOUND, req.response_status
370
287
 
@@ -396,16 +313,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
396
313
  }
397
314
  oauth_signin_id = @store.store(auth_params)
398
315
 
399
- req = @test_harness.request(
400
- {
401
- ':method' => 'POST',
402
- ':path' => '/oauth/consent',
403
- 'cookie' => "oauth_signin_id=#{oauth_signin_id}",
404
- 'content-type' => 'application/x-www-form-urlencoded'
405
- },
406
- URI.encode_www_form(
407
- decision: 'allow',
408
- )
316
+ req = post_form(
317
+ '/oauth/consent',
318
+ { decision: 'allow' },
319
+ 'cookie' => "oauth_signin_id=#{oauth_signin_id}"
409
320
  )
410
321
  assert_equal HTTP::FOUND, req.response_status
411
322
 
@@ -452,19 +363,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
452
363
  end
453
364
 
454
365
  def test_oauth_token_exchange
455
- req = @test_harness.request(
366
+ req = post_form(
367
+ '/oauth/token',
456
368
  {
457
- ':method' => 'POST',
458
- ':path' => '/oauth/token',
459
- 'content-type' => 'application/x-www-form-urlencoded'
460
- },
461
- URI.encode_www_form(
462
- grant_type: 'authorization_code',
463
- code: @auth_code,
464
- redirect_uri: 'http://localhost:4321/callback',
465
- client_id: @client_id,
466
- code_verifier: @code_verifier
467
- )
369
+ grant_type: 'authorization_code',
370
+ code: @auth_code,
371
+ redirect_uri: 'http://localhost:4321/callback',
372
+ client_id: @client_id,
373
+ code_verifier: @code_verifier
374
+ }
468
375
  )
469
376
 
470
377
  assert_equal HTTP::OK, req.response_status
@@ -483,19 +390,8 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
483
390
  end
484
391
 
485
392
  def test_oauth_token_exchange_missing_params
486
- req = @test_harness.request(
487
- {
488
- ':method' => 'POST',
489
- ':path' => '/oauth/token',
490
- 'content-type' => 'application/x-www-form-urlencoded'
491
- },
492
- URI.encode_www_form({}
493
- # grant_type: 'authorization_code',
494
- # code: auth_code,
495
- # redirect_uri: 'http://localhost:4321/callback',
496
- # client_id: client_id,
497
- # code_verifier: code_verifier
498
- )
393
+ req = post_form(
394
+ '/oauth/token', {}
499
395
  )
500
396
 
501
397
  assert_equal HTTP::BAD_REQUEST, req.response_status
@@ -507,19 +403,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
507
403
  end
508
404
 
509
405
  def test_oauth_token_exchange_invalid_grant_type
510
- req = @test_harness.request(
406
+ req = post_form(
407
+ '/oauth/token',
511
408
  {
512
- ':method' => 'POST',
513
- ':path' => '/oauth/token',
514
- 'content-type' => 'application/x-www-form-urlencoded'
515
- },
516
- URI.encode_www_form(
517
409
  grant_type: 'foo',
518
410
  code: @auth_code,
519
411
  redirect_uri: 'http://localhost:4321/callback',
520
412
  client_id: @client_id,
521
413
  code_verifier: @code_verifier
522
- )
414
+ }
523
415
  )
524
416
 
525
417
  assert_equal HTTP::BAD_REQUEST, req.response_status
@@ -531,19 +423,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
531
423
  end
532
424
 
533
425
  def test_oauth_token_exchange_invalid_code
534
- req = @test_harness.request(
426
+ req = post_form(
427
+ '/oauth/token',
535
428
  {
536
- ':method' => 'POST',
537
- ':path' => '/oauth/token',
538
- 'content-type' => 'application/x-www-form-urlencoded'
539
- },
540
- URI.encode_www_form(
541
- grant_type: 'authorization_code',
542
- code: @auth_code + '!',
543
- redirect_uri: 'http://localhost:4321/callback',
544
- client_id: @client_id,
545
- code_verifier: @code_verifier
546
- )
429
+ grant_type: 'authorization_code',
430
+ code: @auth_code + '!',
431
+ redirect_uri: 'http://localhost:4321/callback',
432
+ client_id: @client_id,
433
+ code_verifier: @code_verifier
434
+ }
547
435
  )
548
436
 
549
437
  assert_equal HTTP::BAD_REQUEST, req.response_status
@@ -555,19 +443,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
555
443
  end
556
444
 
557
445
  def test_oauth_token_exchange_invalid_redirect_uri
558
- req = @test_harness.request(
446
+ req = post_form(
447
+ '/oauth/token',
559
448
  {
560
- ':method' => 'POST',
561
- ':path' => '/oauth/token',
562
- 'content-type' => 'application/x-www-form-urlencoded'
563
- },
564
- URI.encode_www_form(
565
- grant_type: 'authorization_code',
566
- code: @auth_code,
567
- redirect_uri: 'http://localhost:4321/foo',
568
- client_id: @client_id,
569
- code_verifier: @code_verifier
570
- )
449
+ grant_type: 'authorization_code',
450
+ code: @auth_code,
451
+ redirect_uri: 'http://localhost:4321/foo',
452
+ client_id: @client_id,
453
+ code_verifier: @code_verifier
454
+ }
571
455
  )
572
456
 
573
457
  assert_equal HTTP::BAD_REQUEST, req.response_status
@@ -579,19 +463,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
579
463
  end
580
464
 
581
465
  def test_oauth_token_exchange_invalid_client_id
582
- req = @test_harness.request(
466
+ req = post_form(
467
+ '/oauth/token',
583
468
  {
584
- ':method' => 'POST',
585
- ':path' => '/oauth/token',
586
- 'content-type' => 'application/x-www-form-urlencoded'
587
- },
588
- URI.encode_www_form(
589
- grant_type: 'authorization_code',
590
- code: @auth_code + '!',
591
- redirect_uri: 'http://localhost:4321/callback',
592
- client_id: @client_id + 'foo',
593
- code_verifier: @code_verifier
594
- )
469
+ grant_type: 'authorization_code',
470
+ code: @auth_code + '!',
471
+ redirect_uri: 'http://localhost:4321/callback',
472
+ client_id: @client_id + 'foo',
473
+ code_verifier: @code_verifier
474
+ }
595
475
  )
596
476
 
597
477
  assert_equal HTTP::BAD_REQUEST, req.response_status
@@ -603,19 +483,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
603
483
  end
604
484
 
605
485
  def test_oauth_token_exchange_invalid_code_verifier
606
- req = @test_harness.request(
486
+ req = post_form(
487
+ '/oauth/token',
607
488
  {
608
- ':method' => 'POST',
609
- ':path' => '/oauth/token',
610
- 'content-type' => 'application/x-www-form-urlencoded'
611
- },
612
- URI.encode_www_form(
613
- grant_type: 'authorization_code',
614
- code: @auth_code + '!',
615
- redirect_uri: 'http://localhost:4321/callback',
616
- client_id: @client_id,
617
- code_verifier: @code_verifier + 'abc'
618
- )
489
+ grant_type: 'authorization_code',
490
+ code: @auth_code + '!',
491
+ redirect_uri: 'http://localhost:4321/callback',
492
+ client_id: @client_id,
493
+ code_verifier: @code_verifier + 'abc'
494
+ }
619
495
  )
620
496
 
621
497
  assert_equal HTTP::BAD_REQUEST, req.response_status
@@ -23,12 +23,11 @@ module Syntropy
23
23
 
24
24
  @done = nil
25
25
  @response_headers = nil
26
+ @response_cookies = nil
26
27
  end
27
28
 
28
29
  def run
29
30
  loop do
30
- @done = nil
31
- @response_headers = nil
32
31
  persist = serve_request
33
32
  break if !persist
34
33
  end
@@ -47,6 +46,9 @@ module Syntropy
47
46
  # object and handing it off to the app handler. Returns true if the
48
47
  # connection should be persisted.
49
48
  def serve_request
49
+ @done = nil
50
+ @response_headers = nil
51
+ @response_cookies = nil
50
52
  @closed = nil
51
53
  headers = @io.http_read_request_headers
52
54
  return false if !headers
@@ -130,13 +132,10 @@ module Syntropy
130
132
  @response_headers ? @response_headers.merge!(headers) : @response_headers = headers
131
133
  end
132
134
 
133
- def set_cookie(*cookies)
134
- existing_cookies = @response_headers && @response_headers['Set-Cookie']
135
- if existing_cookies
136
- @response_headers['Set-Cookie'] = existing_cookies + cookies
137
- else
138
- set_response_headers('Set-Cookie' => cookies)
139
- end
135
+ DELETE_COOKIE = "; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; Max-Age=0; HttpOnly"
136
+
137
+ def set_cookie(key, value)
138
+ (@response_cookies ||= {})[key] = value || DELETE_COOKIE
140
139
  end
141
140
 
142
141
  SEND_FLAGS = UM::MSG_NOSIGNAL | UM::MSG_WAITALL
@@ -152,6 +151,7 @@ module Syntropy
152
151
  # @param body [String] response body
153
152
  # @param headers
154
153
  def respond(request, body, headers)
154
+ add_set_cookie_headers if @response_cookies
155
155
  headers = @response_headers.merge(headers) if @response_headers
156
156
 
157
157
  formatted_headers = format_headers(headers, body)
@@ -315,6 +315,12 @@ module Syntropy
315
315
  lines << "#{key}: #{value}\r\n"
316
316
  end
317
317
  end
318
+
319
+ def add_set_cookie_headers
320
+ @response_headers ||= {}
321
+ sc = (@response_headers['Set-Cookie'] ||= [])
322
+ @response_cookies.each { |k, v| sc << "#{k}=#{v}" }
323
+ end
318
324
  end
319
325
  end
320
326
  end
@@ -39,11 +39,12 @@ module Syntropy
39
39
  # @return [any] export value
40
40
  def load(ref, raise_on_missing: true)
41
41
  ref = "/#{ref}" if ref !~ /^\//
42
+ if !(entry = @modules[ref])
43
+ entry = load_module(ref, raise_on_missing:)
44
+ return if !entry
42
45
 
43
- entry = load_module(ref, raise_on_missing:)
44
- return if !entry
45
-
46
- @modules[ref] ||= entry
46
+ @modules[ref] = entry
47
+ end
47
48
  entry[:export_value]
48
49
  end
49
50
 
@@ -105,8 +105,8 @@ module Syntropy
105
105
  adapter.set_response_headers(headers)
106
106
  end
107
107
 
108
- def set_cookie(*)
109
- adapter.set_cookie(*)
108
+ def set_cookie(k, v)
109
+ adapter.set_cookie(k, v)
110
110
  end
111
111
 
112
112
  def upgrade(protocol, custom_headers = nil, &block)
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'json'
5
+ require 'securerandom'
6
+
7
+ module Syntropy
8
+ class Session
9
+ def initialize(request)
10
+ @request = request
11
+ @data = nil
12
+ end
13
+
14
+ def [](key)
15
+ @data ||= load
16
+ @data[key]
17
+ end
18
+
19
+ def []=(key, value)
20
+ @data ||= load
21
+ @data[key] = value
22
+ save(@data)
23
+ end
24
+
25
+ def delete(key)
26
+ @data ||= load
27
+ @data.delete(key)
28
+ save(@data.empty? ? nil : @data)
29
+ end
30
+
31
+ def discard
32
+ save(nil)
33
+ end
34
+
35
+ def flash
36
+ @data ||= load
37
+ @flash ||= Flash.new(self)
38
+ end
39
+
40
+ private
41
+
42
+ class NowFlash
43
+ def initialize
44
+ @data = {}
45
+ end
46
+
47
+ def [](key)
48
+ @data[key.to_s]
49
+ end
50
+
51
+ def []=(key, value)
52
+ @data[key.to_s] = value
53
+ end
54
+
55
+ def each(&block)
56
+ @data.each { |k, v| block.(k.to_sym, v) }
57
+ end
58
+ end
59
+
60
+ class Flash
61
+ def initialize(session)
62
+ @session = session
63
+ @current_flash_data = @session['_flash']
64
+ @session.delete('_flash') if @current_flash_data
65
+ @current_flash_data ||= {}
66
+ @future_flash_data = {}
67
+ @now_flash_data = NowFlash.new
68
+ end
69
+
70
+ def [](key)
71
+ key = key.to_s
72
+ @now_flash_data[key] || @current_flash_data[key]
73
+ end
74
+
75
+ def []=(key, value)
76
+ key = key.to_s
77
+ @future_flash_data[key] = value
78
+ @session['_flash'] = @future_flash_data
79
+ end
80
+
81
+ def each(&block)
82
+ @now_flash_data.each { |k, v| block.(k.to_sym, v) }
83
+ @current_flash_data.each_pair { |k, v| block.(k.to_sym, v) }
84
+ end
85
+
86
+ def keep
87
+ @future_flash_data = @current_flash_data.merge!(@future_flash_data)
88
+ @session['_flash'] = @future_flash_data
89
+ end
90
+
91
+ def now
92
+ @now_flash_data
93
+ end
94
+ end
95
+
96
+ # Loads session data from
97
+ def load
98
+ data = @request.cookies['__syntropy_session__']
99
+ return {} if !data
100
+
101
+ JSON.parse(Base64.decode64(data))
102
+ rescue JSON::ParserError
103
+ {}
104
+ ensure
105
+ @loaded = true
106
+ end
107
+
108
+ def save(data)
109
+ cookie = data ? "#{Base64.strict_encode64(JSON.dump(data))}; Path=/; HttpOnly" : nil
110
+ @request.set_cookie('__syntropy_session__', cookie)
111
+ end
112
+ end
113
+ end