tep 0.11.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 (193) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Makefile +134 -0
  4. data/README.md +247 -0
  5. data/SINATRA_COMPAT.md +376 -0
  6. data/bin/tep +2156 -0
  7. data/examples/agentic_chat/README.md +103 -0
  8. data/examples/agentic_chat/app.rb +310 -0
  9. data/examples/api_gateway/README.md +49 -0
  10. data/examples/api_gateway/app.rb +66 -0
  11. data/examples/blog/app.rb +367 -0
  12. data/examples/blog/views/index.erb +36 -0
  13. data/examples/blog/views/login.erb +28 -0
  14. data/examples/blog/views/new_post.erb +25 -0
  15. data/examples/blog/views/show.erb +16 -0
  16. data/examples/chat/app.rb +278 -0
  17. data/examples/chat/assets/logo.svg +13 -0
  18. data/examples/chat/assets/style.css +209 -0
  19. data/examples/chat/views/index.erb +142 -0
  20. data/examples/chatbot/README.md +111 -0
  21. data/examples/chatbot/app.rb +1024 -0
  22. data/examples/chatbot/assets/chat.js +249 -0
  23. data/examples/chatbot/assets/compare.js +93 -0
  24. data/examples/chatbot/assets/markdown.js +84 -0
  25. data/examples/chatbot/assets/style.css +215 -0
  26. data/examples/chatbot/schema.sql +25 -0
  27. data/examples/chatbot/views/compare.erb +43 -0
  28. data/examples/chatbot/views/index.erb +42 -0
  29. data/examples/chatbot/views/login.erb +22 -0
  30. data/examples/chatbot/views/setup.erb +23 -0
  31. data/examples/counter/README.md +68 -0
  32. data/examples/counter/app.rb +85 -0
  33. data/examples/experiments/AGENTS.md +91 -0
  34. data/examples/experiments/README.md +99 -0
  35. data/examples/experiments/app.rb +225 -0
  36. data/examples/geohash/Gemfile +11 -0
  37. data/examples/geohash/Gemfile.lock +17 -0
  38. data/examples/geohash/README.md +58 -0
  39. data/examples/geohash/app.rb +33 -0
  40. data/examples/hello.rb +120 -0
  41. data/examples/llm_gateway/README.md +73 -0
  42. data/examples/llm_gateway/app.rb +91 -0
  43. data/examples/maidenhead/Gemfile +7 -0
  44. data/examples/maidenhead/Gemfile.lock +17 -0
  45. data/examples/maidenhead/README.md +47 -0
  46. data/examples/maidenhead/app.rb +46 -0
  47. data/examples/pg_hello.rb +76 -0
  48. data/examples/qdrant/Gemfile +11 -0
  49. data/examples/qdrant/Gemfile.lock +29 -0
  50. data/examples/qdrant/README.md +54 -0
  51. data/examples/sinatra_style.rb +32 -0
  52. data/examples/websocket_echo.rb +37 -0
  53. data/lib/tep/agent_delegation.rb +35 -0
  54. data/lib/tep/app.rb +291 -0
  55. data/lib/tep/assets.rb +52 -0
  56. data/lib/tep/auth.rb +78 -0
  57. data/lib/tep/auth_bearer_token.rb +126 -0
  58. data/lib/tep/auth_oauth2.rb +189 -0
  59. data/lib/tep/auth_oauth2_client.rb +29 -0
  60. data/lib/tep/auth_oauth2_code.rb +40 -0
  61. data/lib/tep/auth_session_cookie.rb +132 -0
  62. data/lib/tep/broadcast.rb +265 -0
  63. data/lib/tep/broadcast_subscription.rb +42 -0
  64. data/lib/tep/cache.rb +49 -0
  65. data/lib/tep/events.rb +257 -0
  66. data/lib/tep/filter.rb +21 -0
  67. data/lib/tep/handler.rb +35 -0
  68. data/lib/tep/http.rb +599 -0
  69. data/lib/tep/identity.rb +67 -0
  70. data/lib/tep/job.rb +186 -0
  71. data/lib/tep/json.rb +572 -0
  72. data/lib/tep/jwt.rb +126 -0
  73. data/lib/tep/live_view.rb +219 -0
  74. data/lib/tep/llm.rb +505 -0
  75. data/lib/tep/logger.rb +85 -0
  76. data/lib/tep/mcp.rb +203 -0
  77. data/lib/tep/multipart.rb +98 -0
  78. data/lib/tep/net.rb +155 -0
  79. data/lib/tep/openai_server.rb +725 -0
  80. data/lib/tep/parallel.rb +168 -0
  81. data/lib/tep/parser.rb +81 -0
  82. data/lib/tep/password.rb +102 -0
  83. data/lib/tep/pg.rb +1128 -0
  84. data/lib/tep/presence.rb +589 -0
  85. data/lib/tep/presence_entry.rb +52 -0
  86. data/lib/tep/proxy.rb +801 -0
  87. data/lib/tep/request.rb +194 -0
  88. data/lib/tep/response.rb +134 -0
  89. data/lib/tep/router.rb +137 -0
  90. data/lib/tep/scheduler.rb +342 -0
  91. data/lib/tep/security.rb +140 -0
  92. data/lib/tep/server.rb +276 -0
  93. data/lib/tep/server_scheduled.rb +375 -0
  94. data/lib/tep/session.rb +98 -0
  95. data/lib/tep/shell.rb +62 -0
  96. data/lib/tep/sphttp.c +858 -0
  97. data/lib/tep/sqlite.rb +215 -0
  98. data/lib/tep/streamer.rb +31 -0
  99. data/lib/tep/tep_pg.c +769 -0
  100. data/lib/tep/tep_sqlite.c +320 -0
  101. data/lib/tep/url.rb +161 -0
  102. data/lib/tep/version.rb +3 -0
  103. data/lib/tep/websocket/connection.rb +171 -0
  104. data/lib/tep/websocket/driver.rb +169 -0
  105. data/lib/tep/websocket/frame.rb +238 -0
  106. data/lib/tep/websocket/handshake.rb +159 -0
  107. data/lib/tep/websocket.rb +68 -0
  108. data/lib/tep.rb +981 -0
  109. data/public/hello.txt +1 -0
  110. data/public/style.css +4 -0
  111. data/spinel-ext.json +33 -0
  112. data/test/helper.rb +248 -0
  113. data/test/real_world/01_simple.rb +5 -0
  114. data/test/real_world/02_lifecycle.rb +20 -0
  115. data/test/real_world/03_chat.rb +75 -0
  116. data/test/real_world/04_health_api.rb +25 -0
  117. data/test/real_world/05_todo_api.rb +57 -0
  118. data/test/real_world/06_basic_auth.rb +25 -0
  119. data/test/real_world/07_bbc_rest_api.rb +228 -0
  120. data/test/real_world/07_sklise_things.rb +109 -0
  121. data/test/real_world/08_jwd83_helloworld.rb +56 -0
  122. data/test/run_all.rb +7 -0
  123. data/test/run_parallel.rb +89 -0
  124. data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
  125. data/test/test_api_gateway.rb +76 -0
  126. data/test/test_auth.rb +223 -0
  127. data/test/test_auth_oauth2.rb +208 -0
  128. data/test/test_auth_session_cookie.rb +198 -0
  129. data/test/test_broadcast.rb +197 -0
  130. data/test/test_broadcast_pg.rb +135 -0
  131. data/test/test_cache.rb +98 -0
  132. data/test/test_cache_static.rb +48 -0
  133. data/test/test_cookies.rb +52 -0
  134. data/test/test_erb.rb +53 -0
  135. data/test/test_erb_ivars.rb +58 -0
  136. data/test/test_events.rb +114 -0
  137. data/test/test_filters.rb +41 -0
  138. data/test/test_geohash_example.rb +89 -0
  139. data/test/test_http.rb +137 -0
  140. data/test/test_http_pool.rb +122 -0
  141. data/test/test_http_pool_send.rb +57 -0
  142. data/test/test_identity.rb +165 -0
  143. data/test/test_inbound_tls.rb +101 -0
  144. data/test/test_inbound_tls_scheduled.rb +101 -0
  145. data/test/test_job.rb +108 -0
  146. data/test/test_json.rb +168 -0
  147. data/test/test_jwt.rb +143 -0
  148. data/test/test_live_view.rb +324 -0
  149. data/test/test_llm.rb +250 -0
  150. data/test/test_llm_gateway.rb +95 -0
  151. data/test/test_logger.rb +101 -0
  152. data/test/test_maidenhead_example.rb +86 -0
  153. data/test/test_mcp.rb +264 -0
  154. data/test/test_misc_v02.rb +54 -0
  155. data/test/test_modular.rb +43 -0
  156. data/test/test_multi_filters.rb +40 -0
  157. data/test/test_mustache.rb +57 -0
  158. data/test/test_openai_server.rb +598 -0
  159. data/test/test_optional_segments.rb +45 -0
  160. data/test/test_parallel.rb +102 -0
  161. data/test/test_params.rb +99 -0
  162. data/test/test_pass.rb +42 -0
  163. data/test/test_password.rb +101 -0
  164. data/test/test_pg.rb +673 -0
  165. data/test/test_presence.rb +374 -0
  166. data/test/test_presence_pg.rb +309 -0
  167. data/test/test_proxy.rb +556 -0
  168. data/test/test_proxy_dsl.rb +119 -0
  169. data/test/test_proxy_streaming.rb +146 -0
  170. data/test/test_real_world.rb +397 -0
  171. data/test/test_regex_routes.rb +52 -0
  172. data/test/test_request_methods.rb +102 -0
  173. data/test/test_response.rb +123 -0
  174. data/test/test_routing.rb +109 -0
  175. data/test/test_scheduler.rb +153 -0
  176. data/test/test_security.rb +72 -0
  177. data/test/test_server_scheduled.rb +56 -0
  178. data/test/test_sessions.rb +59 -0
  179. data/test/test_shell.rb +54 -0
  180. data/test/test_sqlite.rb +148 -0
  181. data/test/test_sqlite_cached.rb +171 -0
  182. data/test/test_static.rb +57 -0
  183. data/test/test_streaming.rb +96 -0
  184. data/test/test_unsupported.rb +32 -0
  185. data/test/test_websocket.rb +152 -0
  186. data/test/test_websocket_echo.rb +138 -0
  187. data/test/views/greet.erb +5 -0
  188. data/test/views/hello.erb +5 -0
  189. data/test/views/list.erb +5 -0
  190. data/test/views/m_ivars.mustache +3 -0
  191. data/test/views/m_simple.mustache +4 -0
  192. data/test/views/mixed.erb +3 -0
  193. metadata +264 -0
@@ -0,0 +1,102 @@
1
+ require_relative "helper"
2
+
3
+ # Tep::Parallel -- fork-based fan-out. Boots a tep app that runs a
4
+ # Worker against a small input list and checks both the
5
+ # result-collecting and fire-and-forget shapes.
6
+ class TestParallel < TepTest
7
+
8
+ app_source <<~RB
9
+ require 'sinatra'
10
+
11
+ class Doubler < Tep::ParallelWorker
12
+ def run(item)
13
+ # Item is a small integer-as-string; double it and emit the
14
+ # child's pid so the test can verify each result came from a
15
+ # distinct process.
16
+ n = item.to_i
17
+ (n * 2).to_s + ":" + Sock.sphttp_getpid.to_s
18
+ end
19
+ end
20
+
21
+ class Echoer < Tep::ParallelWorker
22
+ def run(item)
23
+ item
24
+ end
25
+ end
26
+
27
+ # Two workers so we can ensure the result order matches the
28
+ # input order even though forks complete out of order.
29
+ get '/map_doubled' do
30
+ items = ["1", "2", "3", "4"]
31
+ p = Tep::Parallel.new(Doubler.new)
32
+ results = p.map_processes(items)
33
+ out = ""
34
+ i = 0
35
+ while i < results.length
36
+ if out.length > 0
37
+ out = out + ","
38
+ end
39
+ out = out + results[i]
40
+ i += 1
41
+ end
42
+ out
43
+ end
44
+
45
+ get '/map_echo' do
46
+ items = ["alpha", "beta", "gamma"]
47
+ p = Tep::Parallel.new(Echoer.new)
48
+ results = p.map_processes(items)
49
+ results.join("|")
50
+ end
51
+
52
+ get '/each' do
53
+ # Fire-and-forget: writes a sentinel file in each child;
54
+ # parent then asserts the files exist.
55
+ items = ["x", "y"]
56
+ p = Tep::Parallel.new(FileSentinel.new)
57
+ p.each_process(items)
58
+ ok = "yes"
59
+ if Tep::Shell.read("/tmp/tep_par_test_each_x").length == 0
60
+ ok = "missing_x"
61
+ end
62
+ if Tep::Shell.read("/tmp/tep_par_test_each_y").length == 0
63
+ ok = "missing_y"
64
+ end
65
+ Tep::Shell.run("rm -f /tmp/tep_par_test_each_x /tmp/tep_par_test_each_y")
66
+ ok
67
+ end
68
+
69
+ class FileSentinel < Tep::ParallelWorker
70
+ def run(item)
71
+ File.write("/tmp/tep_par_test_each_" + item, "done")
72
+ ""
73
+ end
74
+ end
75
+ RB
76
+
77
+ def test_map_processes_returns_ordered_results
78
+ res = get("/map_doubled")
79
+ assert_equal "200", res.code
80
+ body = res.body
81
+ parts = body.split(",")
82
+ assert_equal 4, parts.length
83
+ # Each entry: "doubled:pid". The doubled values must be 2,4,6,8.
84
+ doubled = parts.map { |s| s.split(":")[0] }
85
+ assert_equal %w[2 4 6 8], doubled
86
+ # The pids should all be distinct -- one process per item.
87
+ pids = parts.map { |s| s.split(":")[1] }
88
+ assert_equal pids.uniq.length, pids.length, "expected distinct child pids, got #{pids.inspect}"
89
+ end
90
+
91
+ def test_map_processes_preserves_strings
92
+ res = get("/map_echo")
93
+ assert_equal "200", res.code
94
+ assert_equal "alpha|beta|gamma", res.body
95
+ end
96
+
97
+ def test_each_process_runs_workers_for_side_effects
98
+ res = get("/each")
99
+ assert_equal "200", res.code
100
+ assert_equal "yes", res.body
101
+ end
102
+ end
@@ -0,0 +1,99 @@
1
+ require_relative "helper"
2
+
3
+ # Sinatra-style params: path captures, query string, form body merging.
4
+ class TestParams < TepTest
5
+ app_source <<~RB
6
+ get '/path/:a/:b' do
7
+ "" + params[:a] + "/" + params[:b]
8
+ end
9
+
10
+ get '/q' do
11
+ "" + params[:foo]
12
+ end
13
+
14
+ get '/multi' do
15
+ "" + params[:a] + "+" + params[:b]
16
+ end
17
+
18
+ post '/form' do
19
+ "" + params[:name] + "=" + params[:age]
20
+ end
21
+
22
+ post '/multipart' do
23
+ "" + params[:name] + "=" + params[:age]
24
+ end
25
+
26
+ get '/encoded/:name' do
27
+ "" + params[:name]
28
+ end
29
+
30
+ get '/missing' do
31
+ v = params[:nope]
32
+ v.length.to_s + ":" + v
33
+ end
34
+
35
+ get '/q-and-path/:id' do
36
+ "" + params[:id] + "+" + params[:tag]
37
+ end
38
+ RB
39
+
40
+ def test_path_capture_two
41
+ res = get("/path/foo/bar")
42
+ assert_equal "foo/bar", res.body
43
+ end
44
+
45
+ def test_query_single
46
+ res = get("/q?foo=hello")
47
+ assert_equal "hello", res.body
48
+ end
49
+
50
+ def test_query_multiple
51
+ res = get("/multi?a=1&b=2")
52
+ assert_equal "1+2", res.body
53
+ end
54
+
55
+ def test_form_body
56
+ res = post("/form", "name=alice&age=30",
57
+ "Content-Type" => "application/x-www-form-urlencoded")
58
+ assert_equal "alice=30", res.body
59
+ end
60
+
61
+ def test_multipart_body
62
+ # Browsers send multipart/form-data for any form using FormData
63
+ # or carrying a file input. The text fields land in req.params;
64
+ # file-upload parts are skipped in v1.
65
+ bnd = "----TepTestBoundary"
66
+ body = "--#{bnd}\r\n" \
67
+ "Content-Disposition: form-data; name=\"name\"\r\n" \
68
+ "\r\n" \
69
+ "alice\r\n" \
70
+ "--#{bnd}\r\n" \
71
+ "Content-Disposition: form-data; name=\"age\"\r\n" \
72
+ "\r\n" \
73
+ "30\r\n" \
74
+ "--#{bnd}--\r\n"
75
+ res = post("/multipart", body,
76
+ "Content-Type" => "multipart/form-data; boundary=#{bnd}")
77
+ assert_equal "alice=30", res.body
78
+ end
79
+
80
+ def test_url_encoded_path
81
+ res = get("/encoded/hello%20world")
82
+ assert_equal "hello world", res.body
83
+ end
84
+
85
+ def test_url_encoded_plus
86
+ res = get("/q?foo=hello+world")
87
+ assert_equal "hello world", res.body
88
+ end
89
+
90
+ def test_missing_param_is_empty_string
91
+ res = get("/missing")
92
+ assert_equal "0:", res.body
93
+ end
94
+
95
+ def test_query_overlays_path
96
+ res = get("/q-and-path/42?tag=ruby")
97
+ assert_equal "42+ruby", res.body
98
+ end
99
+ end
data/test/test_pass.rb ADDED
@@ -0,0 +1,42 @@
1
+ require_relative "helper"
2
+
3
+ # `pass` skips to the next matching route. Sinatra raises
4
+ # Sinatra::Pass internally; tep just sets a flag and the dispatcher
5
+ # walks to the next match.
6
+ class TestPass < TepTest
7
+ app_source <<~RB
8
+ require 'sinatra'
9
+
10
+ # First definition wins, but it can pass for `/admin/special`.
11
+ get '/admin/:section' do
12
+ pass if params[:section] == "special"
13
+ "default admin: " + params[:section]
14
+ end
15
+
16
+ get '/admin/special' do
17
+ "special admin handler"
18
+ end
19
+
20
+ # If every match passes, default 404 fires.
21
+ get '/skip' do
22
+ pass
23
+ end
24
+ RB
25
+
26
+ def test_pass_falls_through_to_next_match
27
+ res = get("/admin/special")
28
+ assert_equal "200", res.code
29
+ assert_equal "special admin handler", res.body
30
+ end
31
+
32
+ def test_no_pass_returns_first_match
33
+ res = get("/admin/users")
34
+ assert_equal "200", res.code
35
+ assert_equal "default admin: users", res.body
36
+ end
37
+
38
+ def test_pass_with_no_more_matches_404s
39
+ res = get("/skip")
40
+ assert_equal "404", res.code
41
+ end
42
+ end
@@ -0,0 +1,101 @@
1
+ require_relative "helper"
2
+
3
+ # Tep::Password -- PBKDF2-SHA256 password hashing.
4
+ class TestPassword < TepTest
5
+ app_source <<~RB
6
+ require 'sinatra'
7
+
8
+ post '/hash' do
9
+ res.headers["Content-Type"] = "text/plain"
10
+ pwd = Tep::Json.get_str(req.raw_body, "password")
11
+ Tep::Password.hash(pwd)
12
+ end
13
+
14
+ post '/verify' do
15
+ res.headers["Content-Type"] = "text/plain"
16
+ pwd = Tep::Json.get_str(req.raw_body, "password")
17
+ hash = Tep::Json.get_str(req.raw_body, "hash")
18
+ Tep::Password.verify(pwd, hash) ? "ok" : "bad"
19
+ end
20
+
21
+ post '/split' do
22
+ res.headers["Content-Type"] = "text/plain"
23
+ parts = Tep::Password.split4(req.raw_body)
24
+ parts[0] + "|" + parts[1] + "|" + parts[2] + "|" + parts[3]
25
+ end
26
+
27
+ post '/random' do
28
+ res.headers["Content-Type"] = "text/plain"
29
+ Crypto.sp_crypto_random_b64url(16)
30
+ end
31
+ RB
32
+
33
+ def issue_hash(pwd)
34
+ post("/hash", %({"password":"#{pwd}"})).body.strip
35
+ end
36
+
37
+ def verify_pwd(pwd, hash)
38
+ body = '{"password":"' + pwd + '","hash":"' + hash + '"}'
39
+ post("/verify", body).body.strip
40
+ end
41
+
42
+ def test_hash_format
43
+ h = issue_hash("hunter2")
44
+ # pbkdf2-sha256$<iters>$<salt>$<derived>
45
+ parts = h.split("$")
46
+ assert_equal 4, parts.length
47
+ assert_equal "pbkdf2-sha256", parts[0]
48
+ assert_equal "200000", parts[1]
49
+ assert parts[2].length > 0, "salt should be non-empty"
50
+ assert parts[3].length > 0, "derived should be non-empty"
51
+ end
52
+
53
+ def test_verify_good_password
54
+ h = issue_hash("hunter2")
55
+ assert_equal "ok", verify_pwd("hunter2", h)
56
+ end
57
+
58
+ def test_verify_wrong_password
59
+ h = issue_hash("hunter2")
60
+ assert_equal "bad", verify_pwd("not-the-password", h)
61
+ end
62
+
63
+ def test_random_salt_per_hash
64
+ h1 = issue_hash("same-password")
65
+ h2 = issue_hash("same-password")
66
+ refute_equal h1, h2, "two hashes of the same password should differ (random salt)"
67
+ # but BOTH must verify
68
+ assert_equal "ok", verify_pwd("same-password", h1)
69
+ assert_equal "ok", verify_pwd("same-password", h2)
70
+ end
71
+
72
+ def test_malformed_hash_returns_bad
73
+ assert_equal "bad", verify_pwd("anything", "not-a-real-hash")
74
+ assert_equal "bad", verify_pwd("anything", "pbkdf2-sha256$bad")
75
+ assert_equal "bad", verify_pwd("anything", "")
76
+ end
77
+
78
+ def test_random_b64url_distinct
79
+ r1 = post("/random", "").body.strip
80
+ r2 = post("/random", "").body.strip
81
+ refute_equal r1, r2
82
+ # 16 bytes -> 22 b64url chars (no padding).
83
+ assert_equal 22, r1.length
84
+ end
85
+
86
+ def test_split4_basic
87
+ res = post("/split", "a$b$c$d")
88
+ assert_equal "a|b|c|d", res.body.strip
89
+ end
90
+
91
+ def test_split4_with_empty_segments
92
+ res = post("/split", "$$c$d")
93
+ assert_equal "||c|d", res.body.strip
94
+ end
95
+
96
+ def test_split4_short_input
97
+ # Fewer than 3 separators -- trailing slots stay "".
98
+ res = post("/split", "x$y")
99
+ assert_equal "x|y||", res.body.strip
100
+ end
101
+ end