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
data/lib/tep/mcp.rb ADDED
@@ -0,0 +1,203 @@
1
+ # Tep::MCP -- runtime helpers for the MCP battery (chunk 5.1).
2
+ #
3
+ # Most of the action happens in the bin/tep translator: each
4
+ # `mcp_tool` declaration generates a per-tool dispatch cmeth + a
5
+ # direct HTTP route, and the translator-emitted dispatcher class
6
+ # at POST /mcp routes JSON-RPC 2.0 messages to those cmeths by
7
+ # name. This file holds the runtime helpers the generated code
8
+ # leans on -- nested-key JSON extraction, result builders, and
9
+ # JSON-RPC envelope formatters.
10
+ #
11
+ # Public surface (chunk 5.1):
12
+ #
13
+ # Tep::MCP.text(s) -> Result with text content
14
+ # Tep::MCP.error(s) -> Result marked isError = true
15
+ # Tep::MCP.nested_extract(j, k) -> sub-JSON string for a nested key
16
+ # Tep::MCP.initialize_envelope(id, name, version)
17
+ # Tep::MCP.tools_list_envelope(id, tools_json)
18
+ # Tep::MCP.tools_call_envelope(id, result)
19
+ # Tep::MCP.unknown_tool_envelope(id, name)
20
+ # Tep::MCP.method_not_found_envelope(id, method)
21
+ #
22
+ # Apps wire the battery via `mcp_tool '...' do ... end` blocks at
23
+ # the top level; bin/tep does the rest. The runtime here stays
24
+ # small + spinel-friendly (no class-hierarchy dispatch, no
25
+ # heterogeneous arrays). See docs/MCP-BATTERY.md for the design.
26
+ module Tep
27
+ module MCP
28
+ # MCP protocol version this server claims to speak. Tracks the
29
+ # 2025-03 ("Streamable HTTP") revision of the spec.
30
+ PROTOCOL_VERSION = "2025-03-26"
31
+
32
+ # Tool result -- carries either a text content block (the only
33
+ # content type supported in chunk 5.1) or an error marker.
34
+ class Result
35
+ attr_accessor :text, :is_error
36
+
37
+ def initialize
38
+ @text = ""
39
+ @is_error = 0
40
+ end
41
+ end
42
+
43
+ def self.text(s)
44
+ r = Tep::MCP::Result.new
45
+ r.text = s
46
+ r.is_error = 0
47
+ r
48
+ end
49
+
50
+ def self.error(s)
51
+ r = Tep::MCP::Result.new
52
+ r.text = s
53
+ r.is_error = 1
54
+ r
55
+ end
56
+
57
+ # Resource read outcome -- a (uri, mime, text) triple wrapped
58
+ # in the resources/read response envelope. Kept as a simple
59
+ # value class (parallel to Result) so spinel tracks the slot
60
+ # types cleanly across module boundaries.
61
+ class ResourceContent
62
+ attr_accessor :uri, :mime, :text
63
+
64
+ def initialize
65
+ @uri = ""
66
+ @mime = "text/plain"
67
+ @text = ""
68
+ end
69
+ end
70
+
71
+ # Build a text-mime resource content block. URI is the
72
+ # resource's identifier (echoed back to the client so clients
73
+ # can correlate the response with the request).
74
+ def self.resource_text(uri, text)
75
+ c = Tep::MCP::ResourceContent.new
76
+ c.uri = uri
77
+ c.mime = "text/plain"
78
+ c.text = text
79
+ c
80
+ end
81
+
82
+ # Pull a nested JSON value out of `json` by top-level key,
83
+ # returning the value's JSON-string form. Used by the
84
+ # translator-emitted dispatcher to dig `params` out of the
85
+ # JSON-RPC envelope, then `arguments` out of params, before
86
+ # handing the arguments sub-object to the per-tool cmeth.
87
+ #
88
+ # Returns "{}" when the key isn't present (so downstream
89
+ # Tep::Json.get_str / get_int calls see an empty object that
90
+ # returns their zero-default cleanly).
91
+ def self.nested_extract(json, key)
92
+ pos = Tep::Json.find_value_start(json, key)
93
+ if pos < 0
94
+ return "{}"
95
+ end
96
+ end_pos = Tep::Json.skip_value(json, pos)
97
+ if end_pos <= pos
98
+ return "{}"
99
+ end
100
+ json[pos, end_pos - pos]
101
+ end
102
+
103
+ # JSON-RPC 2.0 response envelope for `initialize`. The MCP
104
+ # client expects serverInfo + capabilities + protocolVersion.
105
+ # capabilities lists which method groups this server speaks.
106
+ def self.initialize_envelope(req_id, server_name, server_version)
107
+ "{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
108
+ "\"result\":{" +
109
+ "\"protocolVersion\":\"" + Tep::MCP::PROTOCOL_VERSION + "\"," +
110
+ "\"capabilities\":{\"tools\":{},\"resources\":{}}," +
111
+ "\"serverInfo\":{" +
112
+ "\"name\":" + Tep::Json.quote(server_name) + "," +
113
+ "\"version\":" + Tep::Json.quote(server_version) +
114
+ "}" +
115
+ "}" +
116
+ "}"
117
+ end
118
+
119
+ # Wrap a pre-built tools-array JSON string into the tools/list
120
+ # response envelope. tools_array_json is the literal `[{...},
121
+ # {...}]` the translator emits at compile time.
122
+ def self.tools_list_envelope(req_id, tools_array_json)
123
+ "{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
124
+ "\"result\":{\"tools\":" + tools_array_json + "}" +
125
+ "}"
126
+ end
127
+
128
+ # Wrap a tool's text + error-flag into the tools/call response
129
+ # envelope. content is a one-element array with a text block.
130
+ # Takes scalars rather than the Result struct directly so spinel
131
+ # tracks the String param locally through json_quote without
132
+ # going through attr_accessor return-type inference.
133
+ def self.tools_call_envelope(req_id, text, is_error)
134
+ is_err_str = "false"
135
+ if is_error == 1
136
+ is_err_str = "true"
137
+ end
138
+ "{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
139
+ "\"result\":{" +
140
+ "\"content\":[" +
141
+ "{\"type\":\"text\",\"text\":" + Tep::Json.quote(text) + "}" +
142
+ "]," +
143
+ "\"isError\":" + is_err_str +
144
+ "}" +
145
+ "}"
146
+ end
147
+
148
+ # Wrap a pre-built resources-array JSON string into the
149
+ # resources/list response envelope. Same shape as
150
+ # tools_list_envelope -- translator emits the array literally
151
+ # at compile time so spinel doesn't need to walk it at runtime.
152
+ def self.resources_list_envelope(req_id, resources_array_json)
153
+ "{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
154
+ "\"result\":{\"resources\":" + resources_array_json + "}" +
155
+ "}"
156
+ end
157
+
158
+ # Wrap a ResourceContent into a resources/read response
159
+ # envelope. contents is a one-element array per MCP spec; the
160
+ # uri / mimeType / text fields are read off as scalars (same
161
+ # spinel-friendly pattern as tools_call_envelope) before being
162
+ # spliced into the JSON.
163
+ def self.resources_read_envelope(req_id, uri, mime, text)
164
+ "{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
165
+ "\"result\":{\"contents\":[" +
166
+ "{\"uri\":" + Tep::Json.quote(uri) + "," +
167
+ "\"mimeType\":" + Tep::Json.quote(mime) + "," +
168
+ "\"text\":" + Tep::Json.quote(text) + "}" +
169
+ "]}" +
170
+ "}"
171
+ end
172
+
173
+ # Error envelope for resources/read on an unknown URI. Same
174
+ # JSON-RPC code as unknown_tool (-32602 invalid params).
175
+ def self.unknown_resource_envelope(req_id, uri)
176
+ "{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
177
+ "\"error\":{\"code\":-32602," +
178
+ "\"message\":" + Tep::Json.quote("unknown resource: " + uri) +
179
+ "}" +
180
+ "}"
181
+ end
182
+
183
+ # Error envelope for tools/call on an unknown tool name. Sent
184
+ # as a JSON-RPC error (-32602 invalid params) per the spec.
185
+ def self.unknown_tool_envelope(req_id, tool_name)
186
+ "{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
187
+ "\"error\":{\"code\":-32602," +
188
+ "\"message\":" + Tep::Json.quote("unknown tool: " + tool_name) +
189
+ "}" +
190
+ "}"
191
+ end
192
+
193
+ # Error envelope for an unrecognized JSON-RPC method. Spec
194
+ # code -32601 (method not found).
195
+ def self.method_not_found_envelope(req_id, method_name)
196
+ "{\"jsonrpc\":\"2.0\",\"id\":" + req_id.to_s + "," +
197
+ "\"error\":{\"code\":-32601," +
198
+ "\"message\":" + Tep::Json.quote("method not found: " + method_name) +
199
+ "}" +
200
+ "}"
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,98 @@
1
+ # Tep::Multipart -- text-field parsing for multipart/form-data bodies.
2
+ #
3
+ # Browser forms submitted via `new FormData(form)` (or any form
4
+ # carrying file inputs) use `Content-Type: multipart/form-data`
5
+ # instead of urlencoded. tep's request layer treats those bodies
6
+ # as String fields here. File-upload parts (any part with a
7
+ # `filename=` header) are skipped in v1 -- the field's bytes
8
+ # don't land in `req.params`. Supporting file uploads needs a
9
+ # different surface (likely `req.files`) plus an NUL-safe byte
10
+ # array, both follow-ups.
11
+ #
12
+ # Public API mirrors Url.parse_query: pass the raw body + the
13
+ # request's Content-Type header value; get back a string-keyed
14
+ # string-valued hash, ready to merge into `req.params`.
15
+ module Tep
16
+ module Multipart
17
+ # Parse `body` against the boundary embedded in `content_type`.
18
+ # Returns an empty hash when the boundary can't be extracted
19
+ # (defensive: caller already checked `req.multipart?`).
20
+ def self.parse(body, content_type)
21
+ h = Tep.str_hash
22
+ bnd = Tep::Multipart.extract_boundary(content_type)
23
+ if bnd.length == 0
24
+ return h
25
+ end
26
+ delim = "--" + bnd
27
+ parts = body.split(delim)
28
+ i = 1 # parts[0] is the prologue before the first delimiter
29
+ while i < parts.length
30
+ part = parts[i]
31
+ # Terminator: a part that starts with "--" is the closing
32
+ # boundary "--<bnd>--<crlf>"; nothing meaningful after it.
33
+ if part.length >= 2 && part[0, 2] == "--"
34
+ return h
35
+ end
36
+ # Strip the CRLF that follows every interior delimiter.
37
+ if part.length >= 2 && part[0, 2] == "\r\n"
38
+ part = part[2, part.length - 2]
39
+ end
40
+ # Strip the CRLF that precedes the next delimiter (every
41
+ # interior part ends with \r\n before the next "--<bnd>").
42
+ if part.length >= 2 && part[part.length - 2, 2] == "\r\n"
43
+ part = part[0, part.length - 2]
44
+ end
45
+ sep = Tep.str_find(part, "\r\n\r\n", 0)
46
+ if sep >= 0
47
+ headers = part[0, sep]
48
+ value = part[sep + 4, part.length - sep - 4]
49
+ name = Tep::Multipart.extract_field_name(headers)
50
+ has_filename = Tep.str_find(headers, "filename=", 0) >= 0
51
+ if name.length > 0 && !has_filename
52
+ h[name] = value
53
+ end
54
+ end
55
+ i += 1
56
+ end
57
+ h
58
+ end
59
+
60
+ # Extract `boundary=...` from a Content-Type value. Handles
61
+ # quoted (`boundary="x"`) and unquoted (`boundary=x;` or
62
+ # `boundary=x` at end of string).
63
+ def self.extract_boundary(content_type)
64
+ at = Tep.str_find(content_type, "boundary=", 0)
65
+ if at < 0
66
+ return ""
67
+ end
68
+ rest = content_type[at + 9, content_type.length - at - 9]
69
+ if rest.length > 0 && rest[0, 1] == "\""
70
+ end_q = Tep.str_find(rest, "\"", 1)
71
+ if end_q < 0
72
+ return ""
73
+ end
74
+ return rest[1, end_q - 1]
75
+ end
76
+ semi = Tep.str_find(rest, ";", 0)
77
+ if semi >= 0
78
+ return rest[0, semi]
79
+ end
80
+ rest
81
+ end
82
+
83
+ # Extract the `name="..."` value from a part's headers blob.
84
+ # Returns "" when no name found.
85
+ def self.extract_field_name(headers)
86
+ at = Tep.str_find(headers, "name=\"", 0)
87
+ if at < 0
88
+ return ""
89
+ end
90
+ start = at + 6
91
+ end_q = Tep.str_find(headers, "\"", start)
92
+ if end_q < 0
93
+ return ""
94
+ end
95
+ headers[start, end_q - start]
96
+ end
97
+ end
98
+ end
data/lib/tep/net.rb ADDED
@@ -0,0 +1,155 @@
1
+ # All FFI plumbing lives at the top level so spinel's name resolver
2
+ # finds it from anywhere in the Tep tree (nested modules confuse it).
3
+ #
4
+ # The `@TEP_SPHTTP_O@` placeholder is substituted by `bin/tep` (or
5
+ # the Makefile) with the absolute path to the built sphttp.o on the
6
+ # current host. Spinel doesn't support `__dir__` or `ENV.fetch` in
7
+ # top-level ffi_cflags, so a build-time substitution is the cleanest
8
+ # portable shape.
9
+ module Sock
10
+ ffi_cflags "@TEP_SPHTTP_O@"
11
+ # Outbound TLS (sphttp_connect_tls) is backed by the system
12
+ # libssl/libcrypto. Linked for every app (like sqlite3 elsewhere);
13
+ # the plaintext path never calls into it, so apps that make no HTTPS
14
+ # requests pay only the link cost, not runtime. See tep#148.
15
+ # (When OpenSSL is off the default path -- macOS/Homebrew -- the build
16
+ # finds it via CPATH/LIBRARY_PATH in the environment, not a cflag
17
+ # here; spinel's ffi_cflags rejects an empty-string placeholder.)
18
+ ffi_lib "ssl"
19
+ ffi_lib "crypto"
20
+
21
+ ffi_func :sphttp_listen, [:int, :int], :int
22
+ ffi_func :sphttp_accept, [:int], :int
23
+ # Non-blocking accept variant used by Tep::Server::Scheduled.
24
+ # Listen fd must be in non-blocking mode (sphttp_set_nonblock).
25
+ # Returns -1 with errno EAGAIN/EWOULDBLOCK if no pending connection.
26
+ ffi_func :sphttp_accept_nb, [:int], :int
27
+ ffi_func :sphttp_read_request, [:int], :int
28
+ ffi_func :sphttp_request_buf, [], :str
29
+ ffi_func :sphttp_request_len, [], :int
30
+ ffi_func :sphttp_drain_body, [:int, :int], :str
31
+ ffi_func :sphttp_write_str, [:int, :str], :int
32
+
33
+ # Binary-safe write + recv pair, used by Tep::WebSocket (and any
34
+ # other caller that needs to send/receive bytes containing 0x00).
35
+ # The recv side mirrors the request_buf / _len accessor pattern.
36
+ # See sphttp.c for the binary-safety contract.
37
+ ffi_func :sphttp_write_bytes, [:int, :str, :int], :int
38
+ ffi_func :sphttp_recv_into_frame, [:int], :int
39
+ ffi_func :sphttp_recv_frame_buf, [], :str
40
+ ffi_func :sphttp_recv_frame_len, [], :int
41
+
42
+ # Shutdown-on-signal plumbing. Install once at server start;
43
+ # sphttp_shutdown_requested polls the flag (sigaction sets it on
44
+ # SIGTERM/SIGINT). The server's accept loop checks the flag after
45
+ # sphttp_accept returns -1 and runs Tep.on_shutdown before exit.
46
+ ffi_func :sphttp_install_term_handlers, [], :int
47
+ ffi_func :sphttp_shutdown_requested, [], :int
48
+
49
+ # Millisecond sleep helper for sub-second pacing. spinel's
50
+ # Tep::Scheduler.pause is integer-second only; this exposes the
51
+ # POSIX nanosleep path. Returns 0 on success, -1 on EINTR. Used by
52
+ # Tep::Proxy's retry-backoff loop.
53
+ ffi_func :sphttp_sleep_ms, [:int], :int
54
+
55
+ # HTTP/1.1 outbound connection pool (chunk 6.7a). Per-process pool
56
+ # keyed by (host, port). checkout returns an idle fd or -1; checkin
57
+ # registers one; close_idle sweeps entries older than idle_seconds.
58
+ # Stat getters fetch one counter each (avoids a struct-return over
59
+ # FFI). See Tep::Http::Pool for the Ruby surface.
60
+ ffi_func :sphttp_pool_checkout, [:str, :int], :int
61
+ ffi_func :sphttp_pool_checkin, [:int, :str, :int], :int
62
+ ffi_func :sphttp_pool_close_idle, [:int], :int
63
+ ffi_func :sphttp_pool_stat_checkouts, [], :int
64
+ ffi_func :sphttp_pool_stat_checkins, [], :int
65
+ ffi_func :sphttp_pool_stat_hits, [], :int
66
+ ffi_func :sphttp_pool_stat_misses, [], :int
67
+
68
+ # uname-based host introspection for the toy/v1 envelope (see
69
+ # docs/events-schema.md). sphttp_os_kind returns lowercased
70
+ # uname.sysname ("linux" / "darwin" / ...); sphttp_arch_kind
71
+ # returns uname.machine as-is ("aarch64" / "x86_64" / ...). Both
72
+ # return "unknown" on uname() failure.
73
+ ffi_func :sphttp_os_kind, [], :str
74
+ ffi_func :sphttp_arch_kind, [], :str
75
+
76
+ # ISO-8601 UTC timestamp for an epoch-seconds value. Used by
77
+ # Tep::Events (toy/v1 envelope) for run_start/run_end wall-clock
78
+ # fields -- spinel's Time.now is integer-epoch only.
79
+ ffi_func :sphttp_iso8601_utc, [:int], :str
80
+ # RFC 1123 GMT date (HTTP Date / Last-Modified / Expires) <-> epoch.
81
+ # parse returns -1 if the string doesn't parse.
82
+ ffi_func :sphttp_http_date, [:int], :str
83
+ ffi_func :sphttp_parse_http_date, [:str], :int
84
+
85
+ ffi_func :sphttp_sendfile, [:int, :str], :int
86
+ ffi_func :sphttp_filesize, [:str], :int
87
+ ffi_func :sphttp_file_mtime, [:str], :int
88
+ ffi_func :sphttp_close, [:int], :int
89
+ ffi_func :sphttp_fork, [], :int
90
+ ffi_func :sphttp_exit, [:int], :int
91
+ ffi_func :sphttp_getpid, [], :int
92
+ ffi_func :sphttp_wait_any, [], :int
93
+ ffi_func :sphttp_write_chunk, [:int, :str], :int
94
+ ffi_func :sphttp_write_chunk_end, [:int], :int
95
+
96
+ # Poll-based I/O readiness, used by Tep::Scheduler.io_wait. Mode
97
+ # bits in/out: 1=READ, 2=WRITE.
98
+ ffi_func :sphttp_poll_reset, [], :int
99
+ ffi_func :sphttp_poll_add, [:int, :int], :int
100
+ ffi_func :sphttp_poll_run, [:int], :int
101
+ ffi_func :sphttp_poll_ready, [:int], :int
102
+ ffi_func :sphttp_set_nonblock, [:int], :int
103
+ # Bound a blocking recv (SO_RCVTIMEO, ms). Used by the pooled
104
+ # outbound client so a no-Content-Length keep-alive response can't
105
+ # hang the worker. 0 clears the timeout.
106
+ ffi_func :sphttp_set_recv_timeout, [:int, :int], :int
107
+
108
+ # Outbound TCP for clients (Tep::Http, etc.).
109
+ ffi_func :sphttp_connect, [:str, :int], :int
110
+ # TLS variant: TCP connect + verified TLS handshake (SNI + hostname
111
+ # + peer cert). Returns an fd whose write/recv/close transparently
112
+ # route through the SSL*. -1 on connect/handshake/verify failure.
113
+ ffi_func :sphttp_connect_tls, [:str, :int], :int
114
+ # Inbound (server) TLS (tep#148 phase 2). server_init loads cert+key
115
+ # once (before prefork); accept_tls wraps an accepted fd in a TLS
116
+ # handshake (0 ok / -1 fail, caller closes). read/write/close then
117
+ # route through the SSL* via the same fd registry.
118
+ ffi_func :sphttp_tls_server_init, [:str, :str], :int
119
+ ffi_func :sphttp_accept_tls, [:int], :int
120
+ # Non-blocking TLS (tep#150 outbound coop + scheduled inbound). *_start
121
+ # sets up the SSL but does NOT run the handshake; handshake_step drives
122
+ # one SSL_do_handshake (0=done / 1=want-read / 2=want-write / -1=fail)
123
+ # so a fiber parks on io_wait between steps. io_status reports the last
124
+ # recv/handshake want-state (0 ok / 1 read / 2 write / 3 eof / -1 err)
125
+ # so the coop recv loops tell a TLS partial record from a real EOF.
126
+ ffi_func :sphttp_tls_connect_start, [:str, :int], :int
127
+ ffi_func :sphttp_tls_accept_start, [:int], :int
128
+ ffi_func :sphttp_tls_handshake_step, [:int], :int
129
+ ffi_func :sphttp_io_status, [], :int
130
+ ffi_func :sphttp_recv_some, [:int, :int], :str
131
+ ffi_func :sphttp_recv_all, [:int, :int], :str
132
+
133
+ # popen-shaped shell capture used by Tep::Shell.run. File I/O goes
134
+ # through spinel's built-in File.read / File.write since master
135
+ # (matz/spinel#505 made File.write binary-safe).
136
+ ffi_func :sphttp_shell_capture, [:str, :int], :str
137
+ end
138
+
139
+ # Crypto FFI -- SHA-256/HMAC/PBKDF2/B64URL/random. Symbols live in
140
+ # spinel's libspinel_rt.a (added upstream as lib/sp_crypto.c via
141
+ # matz/spinel#514), which the spinel driver auto-links into every
142
+ # binary. No ffi_cflags needed; just declare the signatures.
143
+ module Crypto
144
+ ffi_func :sp_crypto_hmac_sha256_hex, [:str, :str], :str
145
+ ffi_func :sp_crypto_hmac_sha256_b64url, [:str, :str], :str
146
+ ffi_func :sp_crypto_b64url_encode, [:str], :str
147
+ ffi_func :sp_crypto_b64url_decode, [:str], :str
148
+ ffi_func :sp_crypto_pbkdf2_sha256_b64url, [:str, :str, :int], :str
149
+ ffi_func :sp_crypto_random_b64url, [:int], :str
150
+ # SHA-1 + WebSocket accept-key compute. SHA-1 is shipped only
151
+ # because RFC 6455 requires it for the Sec-WebSocket-Accept
152
+ # derivation; do NOT use it for anything else (collision-broken).
153
+ ffi_func :sp_crypto_sha1_hex, [:str], :str
154
+ ffi_func :sp_crypto_websocket_accept, [:str], :str
155
+ end