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,98 @@
1
+ # Tep::Session -- string-keyed string store, persisted in a signed
2
+ # cookie. Format: `urlencoded_payload.hexhmac` where the signature
3
+ # covers exactly the urlencoded payload. Forgery-resistant given a
4
+ # strong secret; payload is *visible* to clients (not encrypted).
5
+ #
6
+ # To enable: set `Tep.session_secret` to a long random string at app
7
+ # load time (e.g. `Tep.session_secret = ENV.fetch("TEP_SESSION_SECRET")`).
8
+ # When unset, sessions silently no-op (read-only Bag, no Set-Cookie).
9
+ module Tep
10
+ COOKIE_NAME = "tep.session"
11
+
12
+ class Session
13
+ attr_accessor :data, :dirty
14
+
15
+ def initialize
16
+ @data = Tep.str_hash
17
+ @dirty = false
18
+ end
19
+
20
+ # Spinel doesn't dispatch user-defined `[]` / `[]=` on user
21
+ # classes -- and emitting them at all forces those methods to
22
+ # default-typed mrb_int params for callers we don't have, which
23
+ # mismatches the underlying String/String slots. So Session
24
+ # exposes only named methods; the translator rewrites
25
+ # `session[k] = v` to `session.set(k, v)` and `session[k]` to
26
+ # `session.get(k)` for source compatibility with Sinatra.
27
+ def get(k); @data[k]; end
28
+ def set(k, v); @data[k] = v; @dirty = true; end
29
+ def has?(k); @data.key?(k); end
30
+ def length; @data.length; end
31
+ def clear; @data = Tep.str_hash; @dirty = true; end
32
+
33
+ # Verify + decode an inbound cookie value. Returns true on
34
+ # success (data populated), false on missing / tampered.
35
+ def load_from(cookie_value, secret)
36
+ if cookie_value.length == 0 || secret.length == 0
37
+ return false
38
+ end
39
+ dot = cookie_value.rindex(".")
40
+ if dot.nil?
41
+ return false
42
+ end
43
+ payload = cookie_value[0, dot]
44
+ sig = cookie_value[dot + 1, cookie_value.length - dot - 1]
45
+ expect = Crypto.sp_crypto_hmac_sha256_hex(secret, payload)
46
+ if !Tep.timing_safe_eq(sig, expect)
47
+ return false
48
+ end
49
+ Url.parse_query(payload).each do |k, v|
50
+ @data[k] = v
51
+ end
52
+ true
53
+ end
54
+
55
+ # Serialize + sign for the response cookie. Caller decides when
56
+ # to call this (typically only when @dirty).
57
+ def to_cookie_value(secret)
58
+ payload = ""
59
+ first = true
60
+ @data.each do |k, v|
61
+ if !first
62
+ payload = payload + "&"
63
+ end
64
+ payload = payload + Url.escape(k) + "=" + Url.escape(v)
65
+ first = false
66
+ end
67
+ payload + "." + Crypto.sp_crypto_hmac_sha256_hex(secret, payload)
68
+ end
69
+ end
70
+
71
+ # Constant-time string equality. Avoids leaking the matching prefix
72
+ # length via early-exit timing. spinel doesn't have a stdlib
73
+ # crypto-safe compare, so we roll our own.
74
+ def self.timing_safe_eq(a, b)
75
+ if a.length != b.length
76
+ return false
77
+ end
78
+ diff = 0
79
+ i = 0
80
+ n = a.length
81
+ while i < n
82
+ # getbyte(i), NOT bytes[i]: `String#bytes` allocates a fresh
83
+ # array on EVERY iteration (O(n^2) garbage). Besides being slow,
84
+ # that allocation storm drives the GC hard enough to free `b` --
85
+ # the HMAC string returned from the Crypto FFI call, held only in
86
+ # an argument local -- mid-loop, so a valid cookie fails its
87
+ # signature check ~5% of the time under load (a #1052-family
88
+ # heap-local rooting gap in spinel, open on master cc94707; the
89
+ # real fix is upstream, tracked at tep#157). getbyte allocates
90
+ # nothing, removing the dominant GC trigger here (cuts the flake
91
+ # ~3x); the residual lives at other unrooted-local sites in the
92
+ # decode path and clears only when spinel roots heap locals.
93
+ diff = diff | (a.getbyte(i) ^ b.getbyte(i))
94
+ i += 1
95
+ end
96
+ diff == 0
97
+ end
98
+ end
data/lib/tep/shell.rb ADDED
@@ -0,0 +1,62 @@
1
+ # Tep::Shell -- minimal popen-based shell-out + /proc-style file
2
+ # reads. The pair covers ~all of what a "system dashboard" needs
3
+ # without dragging in an Open3-equivalent.
4
+ #
5
+ # Security note
6
+ # -------------
7
+ # `run(cmd)` passes its argument verbatim to `/bin/sh -c`. NEVER
8
+ # interpolate untrusted input into the command string -- you'll get
9
+ # a textbook command injection. The same is true of every other
10
+ # popen-style API in any language; we don't pretend otherwise.
11
+ #
12
+ # When you need to feed user-controllable values to a command, build
13
+ # the argv yourself, write to a temp file, or use an explicit allow-
14
+ # list of acceptable inputs.
15
+ module Tep
16
+ class Shell
17
+ DEFAULT_MAX = 65535
18
+
19
+ # Run `cmd` via /bin/sh -c; return up to DEFAULT_MAX bytes of
20
+ # stdout as a string. Stderr is inherited (visible on the
21
+ # server's console / log). The command's exit status is
22
+ # discarded -- callers that need it can append `; echo "EX=$?"`
23
+ # and parse the tail.
24
+ # `+ ""` forces a Ruby-side copy of the static C buffer; see
25
+ # `read` below.
26
+ def self.run(cmd)
27
+ Sock.sphttp_shell_capture(cmd, DEFAULT_MAX) + ""
28
+ end
29
+
30
+ # As above but with a caller-chosen byte cap. Lower caps are
31
+ # cheaper memory-wise; higher caps (up to the sphttp internal
32
+ # buffer of ~64KB) let longer outputs through.
33
+ def self.run_limited(cmd, max_bytes)
34
+ Sock.sphttp_shell_capture(cmd, max_bytes) + ""
35
+ end
36
+
37
+ # Read a file's contents. Useful for /proc/loadavg, /proc/meminfo,
38
+ # /sys/class/thermal/.../temp, and similar small-text endpoints.
39
+ # Returns "" on open failure (spinel's File.read swallows fopen
40
+ # errors and returns the empty string -- matches the prior
41
+ # sphttp_file_read behaviour).
42
+ def self.read(path)
43
+ File.read(path)
44
+ end
45
+
46
+ # Bounded read: slice after the fact. The cap is mostly a
47
+ # defensive cue -- callers that need it should be reading
48
+ # bounded /proc files anyway.
49
+ def self.read_limited(path, max_bytes)
50
+ out = File.read(path)
51
+ out.length > max_bytes ? out[0, max_bytes] : out
52
+ end
53
+
54
+ # Write `data` to `path` (truncate + rewrite). Returns the byte
55
+ # count for symmetry with the old FFI shape; spinel's File.write
56
+ # is void, so we recover it from data.length.
57
+ def self.write(path, data)
58
+ File.write(path, data)
59
+ data.length
60
+ end
61
+ end
62
+ end