syntropy 0.30.0 → 0.31.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/bin/syntropy +8 -86
  4. data/cmd/_banner.rb +16 -0
  5. data/cmd/help.rb +12 -0
  6. data/cmd/serve.rb +95 -0
  7. data/cmd/test.rb +40 -0
  8. data/examples/{counter.rb → basic/counter.rb} +1 -1
  9. data/examples/{templates.rb → basic/templates.rb} +1 -1
  10. data/examples/mcp-oauth/.ruby-version +1 -0
  11. data/examples/mcp-oauth/Gemfile +8 -0
  12. data/examples/mcp-oauth/README.md +128 -0
  13. data/examples/mcp-oauth/app/.well-known/oauth-authorization-server.rb +18 -0
  14. data/examples/mcp-oauth/app/.well-known/oauth-protected-resource.rb +10 -0
  15. data/examples/mcp-oauth/app/_lib/auth_store.rb +23 -0
  16. data/examples/mcp-oauth/app/index.md +1 -0
  17. data/examples/mcp-oauth/app/mcp.rb +38 -0
  18. data/examples/mcp-oauth/app/oauth/authorize.rb +26 -0
  19. data/examples/mcp-oauth/app/oauth/consent.rb +86 -0
  20. data/examples/mcp-oauth/app/oauth/register.rb +15 -0
  21. data/examples/mcp-oauth/app/oauth/token.rb +79 -0
  22. data/examples/mcp-oauth/app/signin.rb +85 -0
  23. data/examples/mcp-oauth/test/helper.rb +9 -0
  24. data/examples/mcp-oauth/test/test_app.rb +27 -0
  25. data/examples/mcp-oauth/test/test_oauth.rb +628 -0
  26. data/lib/syntropy/app.rb +15 -4
  27. data/lib/syntropy/applets/builtin/default_error_handler.rb +3 -3
  28. data/lib/syntropy/applets/builtin/req.rb +1 -1
  29. data/lib/syntropy/dev_mode.rb +1 -1
  30. data/lib/syntropy/errors.rb +6 -0
  31. data/lib/syntropy/http/client.rb +43 -0
  32. data/lib/syntropy/http/client_connection.rb +36 -0
  33. data/lib/syntropy/http/io_extensions.rb +148 -0
  34. data/lib/syntropy/http/server.rb +5 -5
  35. data/lib/syntropy/http/{connection.rb → server_connection.rb} +9 -38
  36. data/lib/syntropy/http.rb +3 -1
  37. data/lib/syntropy/logger.rb +5 -1
  38. data/lib/syntropy/papercraft_extensions.rb +1 -1
  39. data/lib/syntropy/request/mock_adapter.rb +2 -0
  40. data/lib/syntropy/request/request_info.rb +20 -1
  41. data/lib/syntropy/request/response.rb +2 -2
  42. data/lib/syntropy/request/validation.rb +10 -3
  43. data/lib/syntropy/routing_tree.rb +2 -1
  44. data/lib/syntropy/test.rb +65 -0
  45. data/lib/syntropy/version.rb +1 -1
  46. data/lib/syntropy.rb +1 -21
  47. data/syntropy.gemspec +1 -2
  48. data/test/app/.well-known/foo.rb +3 -0
  49. data/test/helper.rb +1 -25
  50. data/test/test_app.rb +53 -68
  51. data/test/test_caching.rb +1 -1
  52. data/test/test_http_client.rb +52 -0
  53. data/test/test_http_client_connection.rb +43 -0
  54. data/test/{test_connection.rb → test_http_server_connection.rb} +29 -29
  55. data/test/test_json_api.rb +4 -4
  56. data/test/{test_request_extensions.rb → test_request.rb} +150 -18
  57. data/test/test_routing_tree.rb +15 -3
  58. metadata +41 -29
  59. data/test/test_request_info.rb +0 -90
  60. /data/examples/{bad.rb → basic/bad.rb} +0 -0
  61. /data/examples/{card.rb → basic/card.rb} +0 -0
  62. /data/examples/{counter.js → basic/counter.js} +0 -0
  63. /data/examples/{counter_api.rb → basic/counter_api.rb} +0 -0
  64. /data/examples/{favicon.ico → basic/favicon.ico} +0 -0
  65. /data/examples/{index.md → basic/index.md} +0 -0
@@ -0,0 +1,79 @@
1
+ AuthStore = import '../_lib/auth_store'
2
+
3
+ # https://datatracker.ietf.org/doc/html/rfc6749#section-5
4
+ export ->(req) do
5
+ req.validate_http_method('post')
6
+ params = req.get_form_data
7
+ req.validate(
8
+ params['redirect_uri'], String,
9
+ message: 'invalid_request'
10
+ )
11
+
12
+ req.validate(
13
+ params['grant_type'], 'authorization_code',
14
+ message: 'unsupported_grant_type'
15
+ )
16
+
17
+ code = params['code']
18
+ auth_info = AuthStore.fetch(code)
19
+ req.validate(
20
+ auth_info, Hash,
21
+ message: 'invalid_request'
22
+ )
23
+
24
+ req.validate(
25
+ params['redirect_uri'], auth_info['redirect_uri'],
26
+ message: 'invalid_request'
27
+ )
28
+
29
+ client_id = params['client_id']
30
+ client_info = AuthStore.fetch(client_id)
31
+ req.validate(
32
+ client_info, Hash,
33
+ message: 'invalid_client'
34
+ )
35
+
36
+ code_verifier = params['code_verifier']
37
+ hashed = Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
38
+ req.validate(
39
+ hashed, auth_info['code_challenge'],
40
+ message: 'invalid_grant'
41
+ )
42
+
43
+ session_info = AuthStore.fetch(auth_info['sid'])
44
+ req.validate(
45
+ session_info, Hash,
46
+ message: 'invalid_grant'
47
+ )
48
+
49
+ token_info = session_info.merge(
50
+ # some app-specific metadata
51
+ type: 'oauth',
52
+ ttl: 86400 * 30
53
+ )
54
+ token = AuthStore.store(token_info)
55
+
56
+ req.respond_json({
57
+ access_token: token,
58
+ token_type: 'Bearer',
59
+ expires_in: token_info[:ttl]
60
+ })
61
+ rescue ValidationError => e
62
+ req.respond_json(
63
+ {
64
+ error: e.message
65
+ },
66
+ ':status' => Syntropy::HTTP::BAD_REQUEST
67
+ )
68
+ rescue => e
69
+ status = Syntropy::Error.http_status(e)
70
+ raise if status == HTTP::INTERNAL_SERVER_ERROR
71
+
72
+ req.respond_json(
73
+ {
74
+ error: 'invalid_request',
75
+ error_description: e.message
76
+ },
77
+ ':status' => Syntropy::HTTP::BAD_REQUEST
78
+ )
79
+ end
@@ -0,0 +1,85 @@
1
+ AuthStore = import '../_lib/auth_store'
2
+
3
+ export ->(req) {
4
+ case req.method
5
+ when 'get'
6
+ render_signin_form(req)
7
+ when 'post'
8
+ validate_signin(req)
9
+ else
10
+ raise Syntropy::Error.method_not_allowed
11
+ end
12
+ }
13
+
14
+ def render_signin_form(req)
15
+ req.respond_html(@signin_form.render)
16
+ end
17
+
18
+ def validate_signin(req)
19
+ creds = req.get_form_data
20
+
21
+ if !valid_creds?(creds)
22
+ req.respond_html(
23
+ @signin_form.render,
24
+ ':status' => Syntropy::HTTP::UNAUTHORIZED
25
+ )
26
+ return
27
+ end
28
+
29
+ sid = AuthStore.store({
30
+ username: creds['username'],
31
+ timestamp: Time.now.to_i
32
+ })
33
+
34
+ oauth_signin_id = req.cookies['oauth_signin_id']
35
+ if oauth_signin_id
36
+ auth_info = AuthStore.fetch(oauth_signin_id)
37
+ AuthStore.update(oauth_signin_id, auth_info.merge('sid' => sid))
38
+ req.validate(auth_info, Hash)
39
+ req.respond(
40
+ nil,
41
+ ':status' => Syntropy::HTTP::SEE_OTHER,
42
+ 'Location' => '/oauth/consent',
43
+ )
44
+ return
45
+ end
46
+
47
+ req.respond(
48
+ nil,
49
+ ':status' => Syntropy::HTTP::SEE_OTHER,
50
+ 'Location' => '/',
51
+ 'Set-Cookie' => "sid=#{sid}"
52
+ )
53
+ end
54
+
55
+ def valid_creds?(creds)
56
+ (creds['username'] == 'foobar') && (creds['password'] == 'foobar')
57
+ end
58
+
59
+ @signin_form = template {
60
+ html {
61
+ head {
62
+ title 'My awesome site'
63
+ }
64
+ body {
65
+ h1 'Sign in:'
66
+
67
+ form(method: 'post') {
68
+ div {
69
+ label 'username:', for: 'username'
70
+ input type: 'text', name: 'username', required: true
71
+ }
72
+
73
+ div {
74
+ label 'password:', for: 'password'
75
+ input type: 'password', name: 'password', required: true
76
+ }
77
+
78
+ div {
79
+ input type: 'submit', value: 'Submit'
80
+ }
81
+ }
82
+ }
83
+ auto_refresh!
84
+ }
85
+ }
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require 'bundler/setup'
4
+ # require 'syntropy'
5
+ # require 'syntropy/test'
6
+ # require 'minitest/autorun'
7
+
8
+ # STDOUT.sync = true
9
+ # STDERR.sync = true
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class AppTest < Minitest::Test
6
+ APP_ROOT = File.expand_path(File.join(__dir__, '../app'))
7
+ HTTP = Syntropy::HTTP
8
+
9
+ def setup
10
+ @machine = UM.new
11
+ @app = Syntropy::App.new(
12
+ root_dir: APP_ROOT,
13
+ mount_path: '/',
14
+ machine: @machine
15
+ )
16
+ @test_harness = Syntropy::TestHarness.new(@app)
17
+ end
18
+
19
+ def test_root
20
+ req = @test_harness.request(
21
+ ':method' => 'GET',
22
+ ':path' => '/'
23
+ )
24
+ assert_equal HTTP::OK, req.response_status
25
+ assert_match /Syntropy/, req.response_body
26
+ end
27
+ end