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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# maidenhead — a tep app that *fully* runs on an external gem (via a Gemfile)
|
|
2
|
+
|
|
3
|
+
This app is built entirely on a real published Ruby gem:
|
|
4
|
+
[`maidenhead` 1.0.1](https://rubygems.org/gems/maidenhead) (MIT), the
|
|
5
|
+
ham-radio [Maidenhead Locator System](https://en.wikipedia.org/wiki/Maidenhead_Locator_System)
|
|
6
|
+
converter. It is **declared in a `Gemfile`** and resolved the proper way —
|
|
7
|
+
not hand-vendored.
|
|
8
|
+
|
|
9
|
+
**Every route exercises the gem, and the entire public API compiles** —
|
|
10
|
+
there is no unsupported-method caveat (contrast the sibling `geohash`
|
|
11
|
+
example, where the gem's hot path works but two helpers need spinel
|
|
12
|
+
features that aren't there yet).
|
|
13
|
+
|
|
14
|
+
## How a tep app uses a gem (the spinelgems convention)
|
|
15
|
+
|
|
16
|
+
tep apps are ahead-of-time compiled by spinel — there is no runtime gem
|
|
17
|
+
loader, and spinel has no gem load path. The
|
|
18
|
+
[`bundler-spinel`](https://github.com/OriPekelman/spinelgems) plugin
|
|
19
|
+
(`spinel-compat`) bridges that: it reads a `Gemfile.lock`, places each
|
|
20
|
+
gem's source under `vendor/spinel/<name>/`, and generates
|
|
21
|
+
`vendor/spinel/deps.rb` (a `require_relative` per gem, in lock order). The
|
|
22
|
+
app pulls everything in with one line:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
require_relative 'vendor/spinel/deps'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`bin/tep build` inlines that `require_relative` **recursively** (deps.rb
|
|
29
|
+
chains to each gem), so the gems become native compiled code in the
|
|
30
|
+
binary. The `Gemfile` + `Gemfile.lock` are the source of truth and are
|
|
31
|
+
committed; `vendor/spinel/` is generated and gitignored.
|
|
32
|
+
|
|
33
|
+
## Build + run
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
make vendor-examples # spinel-compat vendor -> vendor/spinel/ (needs ../spinelgems)
|
|
37
|
+
bin/tep build examples/maidenhead/app.rb -o /tmp/maidenhead
|
|
38
|
+
/tmp/maidenhead -p 4981 &
|
|
39
|
+
|
|
40
|
+
curl 'http://127.0.0.1:4981/valid?loc=FN31pr' # => true
|
|
41
|
+
curl 'http://127.0.0.1:4981/to_latlon?loc=FN31pr' # => 41.731076,-72.704514
|
|
42
|
+
curl 'http://127.0.0.1:4981/to_grid?lat=40.7128&lon=-74.0060&precision=3' # => FN20xr
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Each result is identical to CRuby's `Maidenhead.*`.
|
|
46
|
+
`test/test_maidenhead_example.rb` runs the vendor step, builds this app,
|
|
47
|
+
and checks every route against CRuby's output.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require 'sinatra'
|
|
2
|
+
|
|
3
|
+
# A tep app built entirely on a REAL published Ruby gem -- maidenhead
|
|
4
|
+
# 1.0.1 (MIT), the ham-radio grid-locator <-> lat/lon converter --
|
|
5
|
+
# declared in a Gemfile and resolved the proper way: `spinel-compat
|
|
6
|
+
# vendor` (bundler-spinel, from ../spinelgems) reads Gemfile.lock and
|
|
7
|
+
# places the gem under vendor/spinel/ with a generated deps.rb. We pull
|
|
8
|
+
# it in with the ONE require below; bin/tep inlines the require_relative
|
|
9
|
+
# chain into the AOT binary. Nothing here is hand-vendored. Run
|
|
10
|
+
# `make vendor` (or see README.md) before building. Unlike the geohash
|
|
11
|
+
# example, EVERY route exercises the gem and the whole public API
|
|
12
|
+
# compiles -- no unsupported-method caveat.
|
|
13
|
+
require_relative 'vendor/spinel/deps'
|
|
14
|
+
|
|
15
|
+
# GET /valid?loc=FN31pr -> "true" / "false"
|
|
16
|
+
get '/valid' do
|
|
17
|
+
if Maidenhead.valid_maidenhead?(params["loc"])
|
|
18
|
+
"true"
|
|
19
|
+
else
|
|
20
|
+
"false"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# GET /to_latlon?loc=FN31pr -> "41.731076,-72.704514"
|
|
25
|
+
get '/to_latlon' do
|
|
26
|
+
r = Maidenhead.to_latlon(params["loc"])
|
|
27
|
+
r[0].to_s + "," + r[1].to_s
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# GET /to_grid?lat=40.7128&lon=-74.0060&precision=3 -> "FN20xr"
|
|
31
|
+
get '/to_grid' do
|
|
32
|
+
lat = params["lat"].to_f
|
|
33
|
+
lon = params["lon"].to_f
|
|
34
|
+
prec = params["precision"].to_i
|
|
35
|
+
if prec <= 0
|
|
36
|
+
prec = 5
|
|
37
|
+
end
|
|
38
|
+
Maidenhead.to_maidenhead(lat, lon, prec)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
get '/' do
|
|
42
|
+
"tep + maidenhead 1.0.1 (a real, unmodified published gem)\n" +
|
|
43
|
+
"try: /valid?loc=FN31pr\n" +
|
|
44
|
+
" /to_latlon?loc=FN31pr\n" +
|
|
45
|
+
" /to_grid?lat=40.7128&lon=-74.0060&precision=3\n"
|
|
46
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Tep::PG smoke test app -- exercises the v1 PG battery surface.
|
|
2
|
+
#
|
|
3
|
+
# GET / - libpq + server version + a SELECT round-trip
|
|
4
|
+
# GET /tables - list user tables in the public schema
|
|
5
|
+
# GET /error - issues a deliberate SELECT against a missing
|
|
6
|
+
# table, demonstrates the v1 result.ok? check
|
|
7
|
+
#
|
|
8
|
+
# Set PG_URL in the environment (default: postgresql:///postgres,
|
|
9
|
+
# the admin DB on localhost via Unix socket).
|
|
10
|
+
#
|
|
11
|
+
# PG_URL=postgresql://postgres:postgres@127.0.0.1/postgres \
|
|
12
|
+
# ./examples/pg_hello -p 4567
|
|
13
|
+
#
|
|
14
|
+
# v1 returns Results-with-ok? rather than raising on error -- spinel's
|
|
15
|
+
# rescue dispatch can't match module-namespaced exception classes
|
|
16
|
+
# today (matz/spinel#627). Once that lands, this example collapses
|
|
17
|
+
# to the AR-shape `rescue PG::Error => e`.
|
|
18
|
+
require_relative "../lib/tep"
|
|
19
|
+
|
|
20
|
+
PG_URL = ENV["PG_URL"] != nil && ENV["PG_URL"].length > 0 ? ENV["PG_URL"] : "postgresql:///postgres"
|
|
21
|
+
|
|
22
|
+
get '/' do
|
|
23
|
+
c = PG.connect(PG_URL)
|
|
24
|
+
if !c.connected?
|
|
25
|
+
res.set_status(503)
|
|
26
|
+
"PG.connect failed: " + c.last_error_message
|
|
27
|
+
else
|
|
28
|
+
sv = c.server_version
|
|
29
|
+
r = c.exec("SELECT 1 AS one, 'hello' AS greeting")
|
|
30
|
+
body = "libpq " + PG.libpq_version + "\n" +
|
|
31
|
+
"server_version: " + sv.to_s + "\n" +
|
|
32
|
+
"row 0: " + r.getvalue(0, 0) + " / " + r.getvalue(0, 1) + "\n"
|
|
33
|
+
r.clear
|
|
34
|
+
c.close
|
|
35
|
+
res.headers["Content-Type"] = "text/plain"
|
|
36
|
+
body
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
get '/tables' do
|
|
41
|
+
c = PG.connect(PG_URL)
|
|
42
|
+
r = c.exec("SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = 'public' ORDER BY tablename")
|
|
43
|
+
out = "tables (" + r.ntuples.to_s + "):\n"
|
|
44
|
+
i = 0
|
|
45
|
+
n = r.ntuples
|
|
46
|
+
while i < n
|
|
47
|
+
out = out + " " + r.getvalue(i, 0) + "\n"
|
|
48
|
+
i += 1
|
|
49
|
+
end
|
|
50
|
+
r.clear
|
|
51
|
+
c.close
|
|
52
|
+
res.headers["Content-Type"] = "text/plain"
|
|
53
|
+
out
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# exec raises the SQLSTATE-mapped PG::Error subclass on failure (the
|
|
57
|
+
# ruby-pg / AR shape). Rescue the leaf (PG::UndefinedTable) or the base
|
|
58
|
+
# (PG::Error); the SQLSTATE / message stay on the connection's last_*.
|
|
59
|
+
get '/error' do
|
|
60
|
+
c = PG.connect(PG_URL)
|
|
61
|
+
out = ""
|
|
62
|
+
begin
|
|
63
|
+
r = c.exec("SELECT * FROM tep_no_such_table")
|
|
64
|
+
r.clear
|
|
65
|
+
out = "unexpected: query succeeded"
|
|
66
|
+
rescue PG::UndefinedTable => e
|
|
67
|
+
out = "rescued PG::UndefinedTable\n" +
|
|
68
|
+
"sqlstate: " + c.last_sqlstate + "\n" +
|
|
69
|
+
"is undefined-table? " + (c.last_sqlstate == "42P01" ? "yes" : "no") + "\n" +
|
|
70
|
+
"is PG::Error? " + (e.is_a?(PG::Error) ? "yes" : "no") + "\n" +
|
|
71
|
+
"message: " + e.message
|
|
72
|
+
end
|
|
73
|
+
c.close
|
|
74
|
+
res.headers["Content-Type"] = "text/plain"
|
|
75
|
+
out
|
|
76
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
source "https://rubygems.org"
|
|
2
|
+
ruby "3.3.0", engine: "spinel", engine_version: "0.0.0"
|
|
3
|
+
|
|
4
|
+
# The rejected end of the spectrum. qdrant-ruby is a real, useful gem --
|
|
5
|
+
# but its transport is Faraday and its request bodies are built as
|
|
6
|
+
# incrementally-mutated heterogeneous hashes, neither of which compiles
|
|
7
|
+
# under spinel today. We declare it the same way as the working examples
|
|
8
|
+
# precisely so the spinelgems GATE (`spinel-compat check`) can tell us so,
|
|
9
|
+
# with reasons -- that is the whole point of running gems through
|
|
10
|
+
# spinelgems. See README.md.
|
|
11
|
+
gem "qdrant-ruby", "0.9.10"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
GEM
|
|
2
|
+
remote: https://rubygems.org/
|
|
3
|
+
specs:
|
|
4
|
+
faraday (2.14.2)
|
|
5
|
+
faraday-net_http (>= 2.0, < 3.5)
|
|
6
|
+
json
|
|
7
|
+
logger
|
|
8
|
+
faraday-net_http (3.4.3)
|
|
9
|
+
net-http (~> 0.5)
|
|
10
|
+
json (2.19.7)
|
|
11
|
+
logger (1.7.0)
|
|
12
|
+
net-http (0.9.1)
|
|
13
|
+
uri (>= 0.11.1)
|
|
14
|
+
qdrant-ruby (0.9.10)
|
|
15
|
+
faraday (>= 2.0.1, < 3)
|
|
16
|
+
uri (1.1.1)
|
|
17
|
+
|
|
18
|
+
PLATFORMS
|
|
19
|
+
aarch64-linux
|
|
20
|
+
ruby
|
|
21
|
+
|
|
22
|
+
DEPENDENCIES
|
|
23
|
+
qdrant-ruby (= 0.9.10)
|
|
24
|
+
|
|
25
|
+
RUBY VERSION
|
|
26
|
+
ruby 3.4.9p82
|
|
27
|
+
|
|
28
|
+
BUNDLED WITH
|
|
29
|
+
2.6.9
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# qdrant — the rejected end of the spectrum (a spinelgems gate experiment)
|
|
2
|
+
|
|
3
|
+
This directory has **no app** — on purpose. It's the counterpart to the
|
|
4
|
+
`geohash` and `maidenhead` examples: a real, useful gem
|
|
5
|
+
([`qdrant-ruby`](https://rubygems.org/gems/qdrant-ruby)) that **cannot** be
|
|
6
|
+
used from a tep app today, declared in a [`Gemfile`](Gemfile) precisely so
|
|
7
|
+
the spinelgems compatibility **gate** can tell us *why*. Running gems
|
|
8
|
+
through spinelgems is the point; a clean "no" with reasons is a useful
|
|
9
|
+
result.
|
|
10
|
+
|
|
11
|
+
## The gate verdict
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bundle lock
|
|
15
|
+
SPINEL_DIR=/spinel ruby -I $SPINELGEMS/lib $SPINELGEMS/exe/spinel-compat check Gemfile.lock
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
✗ faraday 2.14.2 rejected — hard:Mutex.new
|
|
20
|
+
✗ faraday-net_http 3.4.3 rejected — no-entrypoint
|
|
21
|
+
✗ json 2.19.7 rejected — c-extension
|
|
22
|
+
✗ logger 1.7.0 rejected — unresolved:instance_method, …
|
|
23
|
+
✗ net-http 0.9.1 rejected — no-entrypoint
|
|
24
|
+
✓ qdrant-ruby 0.9.10 clean
|
|
25
|
+
✗ uri 1.1.1 rejected — unresolved:…
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The gem's *own* source is `clean` — but it's unusable, because its entire
|
|
29
|
+
transitive transport stack is rejected:
|
|
30
|
+
|
|
31
|
+
- **faraday** uses `Mutex.new`, which spinel doesn't support (`hard:`).
|
|
32
|
+
- **json** is a C-extension gem (spinel provides its own `JSON`, but the
|
|
33
|
+
gem's native build can't be compiled in).
|
|
34
|
+
- **net-http / faraday-net_http** have no spinel entrypoint.
|
|
35
|
+
- **uri / logger** lean on dozens of unresolved stdlib methods.
|
|
36
|
+
|
|
37
|
+
So unlike `geohash` (gem reuse is per-*method*), qdrant fails at the
|
|
38
|
+
*dependency* level: there's no subset of entry points that avoids Faraday.
|
|
39
|
+
|
|
40
|
+
## What we proved separately
|
|
41
|
+
|
|
42
|
+
Even bypassing the gem (raw `Tep::Http` + `JSON.generate`), the Qdrant
|
|
43
|
+
*write* path is blocked by a spinel codegen gap: request bodies are built
|
|
44
|
+
as incrementally-mutated **heterogeneous** hashes (`h["a"]=1; h["b"]="x"`),
|
|
45
|
+
which spinel types homogeneously and fails to compile. A tep-native client
|
|
46
|
+
that builds bodies as *literals* does round-trip against Qdrant Cloud over
|
|
47
|
+
TLS — but that's not "using the gem". See the project memory notes.
|
|
48
|
+
|
|
49
|
+
## Why this is here
|
|
50
|
+
|
|
51
|
+
A gem catalog is only trustworthy if the "no"s are as legible as the
|
|
52
|
+
"yes"es. `geohash` + `maidenhead` show what works; `qdrant` shows the gate
|
|
53
|
+
catching what doesn't, with actionable reasons. All three go through the
|
|
54
|
+
same Gemfile + `spinel-compat` path.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Sinatra-classic style. Compile with `bin/tep build sinatra_style.rb`.
|
|
2
|
+
# This file is NOT meant to be passed to spinel directly -- the translator
|
|
3
|
+
# rewrites the do/end blocks into Tep::Handler subclasses first.
|
|
4
|
+
require 'sinatra' # ignored by translator; documentation-only
|
|
5
|
+
|
|
6
|
+
set :public_dir, '../public'
|
|
7
|
+
|
|
8
|
+
get '/' do
|
|
9
|
+
"<h1>hello, world</h1>" +
|
|
10
|
+
"<p>This is real Sinatra-style source compiled by spinel via tep.</p>"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
get '/hi/:name' do
|
|
14
|
+
"<p>hi, " + params[:name] + "!</p>"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
get '/about' do
|
|
18
|
+
content_type 'text/plain'
|
|
19
|
+
"served as plain text\n"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
get '/old' do
|
|
23
|
+
redirect '/'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
before do
|
|
27
|
+
puts "[" + request.verb + "] " + request.path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
not_found do
|
|
31
|
+
"<h1>oops -- " + request.path + " not here</h1>"
|
|
32
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Tep WebSocket echo demo.
|
|
2
|
+
#
|
|
3
|
+
# Walks the Sinatra-shaped DSL hook the bin/tep translator lowers
|
|
4
|
+
# into a generated upgrade route + per-event Tep::WebSocket::Handler
|
|
5
|
+
# subclasses. WS support requires the scheduled server (the recv
|
|
6
|
+
# loop parks on Tep::Scheduler.io_wait), so we opt in via
|
|
7
|
+
# `set :scheduler, :scheduled`.
|
|
8
|
+
#
|
|
9
|
+
# Try it:
|
|
10
|
+
# ./examples/websocket_echo -p 4567
|
|
11
|
+
# # then from a separate terminal:
|
|
12
|
+
# websocat ws://127.0.0.1:4567/echo
|
|
13
|
+
# > hello
|
|
14
|
+
# < echo: hello
|
|
15
|
+
require_relative "../lib/tep"
|
|
16
|
+
|
|
17
|
+
set :scheduler, :scheduled
|
|
18
|
+
|
|
19
|
+
get "/" do
|
|
20
|
+
"<!doctype html><html><body>" +
|
|
21
|
+
"<p>WebSocket echo server. Connect to <code>ws://host:port/echo</code>.</p>" +
|
|
22
|
+
"</body></html>"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
websocket "/echo" do |ws|
|
|
26
|
+
on_open do |evt|
|
|
27
|
+
ws.text("welcome")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
on_message do |evt|
|
|
31
|
+
ws.text("echo: " + evt.data)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
on_close do |evt|
|
|
35
|
+
# No-op; placeholder for the user's cleanup path.
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Tep::AgentDelegation -- the "on behalf of" half of a delegated
|
|
2
|
+
# identity. Carries the agent's own id (distinct from the human
|
|
3
|
+
# principal it acts for), the grant timestamps, and the origin
|
|
4
|
+
# label so audit logs can tell apart "issued via OAuth consent" vs
|
|
5
|
+
# "minted from a session handoff" vs "raw API token".
|
|
6
|
+
#
|
|
7
|
+
# Always paired with a Tep::Identity whose principal_id is the
|
|
8
|
+
# human being acted for. An Identity#human? has acting_via == nil;
|
|
9
|
+
# an Identity#agent? has acting_via populated with this struct.
|
|
10
|
+
#
|
|
11
|
+
# Lives in its own file so consumers that want the delegation
|
|
12
|
+
# vocabulary without the full Identity surface can require it
|
|
13
|
+
# narrowly.
|
|
14
|
+
module Tep
|
|
15
|
+
class AgentDelegation
|
|
16
|
+
attr_reader :agent_id # String, e.g. "summarizer-bot"
|
|
17
|
+
attr_reader :issued_at # Integer (unix epoch seconds)
|
|
18
|
+
attr_reader :expires_at # Integer (unix epoch seconds)
|
|
19
|
+
attr_reader :origin # Symbol: :token, :oauth_grant, :session_handoff, ...
|
|
20
|
+
|
|
21
|
+
def initialize(agent_id, issued_at, expires_at, origin)
|
|
22
|
+
@agent_id = agent_id
|
|
23
|
+
@issued_at = issued_at
|
|
24
|
+
@expires_at = expires_at
|
|
25
|
+
@origin = origin
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# `now` is unix epoch seconds (Time.now.to_i shape). Passed in
|
|
29
|
+
# rather than read from Time.now so callers control the clock
|
|
30
|
+
# source (and tests can fast-forward).
|
|
31
|
+
def expired?(now)
|
|
32
|
+
now >= @expires_at
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/tep/app.rb
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Tep::App -- the registered route table + filter slots + 404 handler.
|
|
2
|
+
#
|
|
3
|
+
# Each filter slot holds a single Tep::Filter instance. Spinel's
|
|
4
|
+
# `PtrArray` is homogeneously-typed and doesn't carry cls_id tags,
|
|
5
|
+
# so an array of mixed Filter subclasses falls through to base-class
|
|
6
|
+
# dispatch (the user's #before / #after never runs). A single slot
|
|
7
|
+
# typed as a union of subclasses keeps virtual dispatch working.
|
|
8
|
+
# Users compose multiple filters by writing one class that calls
|
|
9
|
+
# the others.
|
|
10
|
+
module Tep
|
|
11
|
+
class App
|
|
12
|
+
attr_accessor :router, :static_root, :session_secret
|
|
13
|
+
attr_accessor :tls_cert, :tls_key
|
|
14
|
+
attr_accessor :before_filter, :after_filter, :nf_handler
|
|
15
|
+
# The auth-filter runs BEFORE before_filter so handler bodies and
|
|
16
|
+
# user filters always see a populated req.identity. Separate slot
|
|
17
|
+
# (rather than wedging into before_filter) so user-installed
|
|
18
|
+
# filters and the auth populate don't fight for the single slot
|
|
19
|
+
# tep otherwise imposes. Default is a no-op Tep::Filter; the
|
|
20
|
+
# Auth battery installs Tep::AuthFilter on top via
|
|
21
|
+
# Tep::Auth.install!.
|
|
22
|
+
attr_accessor :auth_filter
|
|
23
|
+
# Shared HS256 secret consumed by Tep::AuthBearerToken. Stored on
|
|
24
|
+
# APP (rather than a class var) so spinel routes the read through
|
|
25
|
+
# the canonical instance-attr path.
|
|
26
|
+
attr_accessor :auth_bearer_secret
|
|
27
|
+
# Per-process OAuth2 client registry + ephemeral authorization-code
|
|
28
|
+
# store. See Tep::AuthOAuth2 for the issuance flow.
|
|
29
|
+
attr_accessor :auth_oauth2_clients
|
|
30
|
+
attr_accessor :auth_oauth2_codes
|
|
31
|
+
# Per-process Broadcast subscriber registry. Each entry pairs a
|
|
32
|
+
# topic with an output fd; publish iterates + writes the payload
|
|
33
|
+
# to every matching fd.
|
|
34
|
+
attr_accessor :broadcast_subs
|
|
35
|
+
# Per-process Presence entry registry. Each entry is one
|
|
36
|
+
# (principal, session, topic) tracking, with kind/agent_id +
|
|
37
|
+
# structured-status fields inline. See Tep::Presence.
|
|
38
|
+
attr_accessor :presence_entries
|
|
39
|
+
# PG-mirror state for cross-worker visibility. `enabled` is 0
|
|
40
|
+
# when off, 1 when on. `worker_id` uniquely identifies this
|
|
41
|
+
# worker's rows in the tep_presence table (PID + boot epoch
|
|
42
|
+
# so a restart on the same PID isn't aliased). See
|
|
43
|
+
# Tep::Presence.enable_pg_mirror.
|
|
44
|
+
attr_accessor :presence_pg_enabled
|
|
45
|
+
attr_accessor :presence_pg_worker_id
|
|
46
|
+
attr_accessor :presence_pg_conn
|
|
47
|
+
# PG-backed cross-worker pub/sub state. `broadcast_pg_enabled`
|
|
48
|
+
# is 0 when off, 1 when on. The dedicated LISTEN connection
|
|
49
|
+
# lives in `broadcast_pg_conn`; channel name in
|
|
50
|
+
# `broadcast_pg_channel`. Configured by
|
|
51
|
+
# Tep::Broadcast.enable_pg_backend.
|
|
52
|
+
attr_accessor :broadcast_pg_enabled
|
|
53
|
+
attr_accessor :broadcast_pg_channel
|
|
54
|
+
attr_accessor :broadcast_pg_conn
|
|
55
|
+
# Tep::Llm::OpenAI::Server backend (Battery 7). Set by
|
|
56
|
+
# Server.use(backend) at boot; the route handlers dispatch through
|
|
57
|
+
# it per request. Seeded with a base Backend in lib/tep.rb (after
|
|
58
|
+
# openai_server.rb loads -- not in initialize, since the class
|
|
59
|
+
# isn't defined yet there), same pattern as broadcast_pg_conn.
|
|
60
|
+
attr_accessor :openai_backend
|
|
61
|
+
# Tep::Events emitter for the openai-server (7.1c). Configured by
|
|
62
|
+
# Server.serve!(events_jsonl); empty path => zero-overhead disabled.
|
|
63
|
+
# Late-seeded for the same reason as openai_backend.
|
|
64
|
+
attr_accessor :openai_events
|
|
65
|
+
attr_accessor :asset_bodies, :asset_mimes, :asset_etags
|
|
66
|
+
attr_accessor :sched_fibers, :sched_wake_at, :sched_current
|
|
67
|
+
attr_accessor :sched_io_fd, :sched_io_mode, :sched_io_ready
|
|
68
|
+
# Tep::Job background-worker idempotency flag. App-level so a
|
|
69
|
+
# single-shot spawn from a before-filter doesn't fire repeatedly.
|
|
70
|
+
# Per-worker (each prefork child has its own Tep::APP, so each
|
|
71
|
+
# worker spawns one background fiber).
|
|
72
|
+
attr_accessor :user_bg_started
|
|
73
|
+
|
|
74
|
+
def initialize
|
|
75
|
+
@router = Router.new
|
|
76
|
+
@static_root = ""
|
|
77
|
+
@session_secret = ""
|
|
78
|
+
@tls_cert = "" # inbound TLS cert path (tep#148 ph2; "" = plain HTTP)
|
|
79
|
+
@tls_key = "" # inbound TLS key path
|
|
80
|
+
@before_filter = Filter.new # no-op default
|
|
81
|
+
@after_filter = Filter.new
|
|
82
|
+
@auth_filter = Filter.new # no-op until Tep::Auth.install!
|
|
83
|
+
@auth_bearer_secret = ""
|
|
84
|
+
# Type-seed the OAuth2 registries with a single dummy entry +
|
|
85
|
+
# immediate drop so the PtrArray slot type is pinned.
|
|
86
|
+
@auth_oauth2_clients = [Tep::AuthOAuth2Client.new("_", "", "", [:_])]
|
|
87
|
+
@auth_oauth2_clients.delete_at(0)
|
|
88
|
+
@auth_oauth2_codes = [Tep::AuthOAuth2Code.new("_", "", "", "", 0)]
|
|
89
|
+
@auth_oauth2_codes.delete_at(0)
|
|
90
|
+
# Same type-seed pattern for the Broadcast subscriber registry.
|
|
91
|
+
@broadcast_subs = [Tep::BroadcastSubscription.new("_", -1, 0)]
|
|
92
|
+
@broadcast_subs.delete_at(0)
|
|
93
|
+
# And for the Presence entry registry.
|
|
94
|
+
@presence_entries = [Tep::PresenceEntry.new("_", "", :human, "", -1, 0)]
|
|
95
|
+
@presence_entries.delete_at(0)
|
|
96
|
+
@presence_pg_enabled = 0
|
|
97
|
+
@presence_pg_worker_id = ""
|
|
98
|
+
@broadcast_pg_enabled = 0
|
|
99
|
+
@broadcast_pg_channel = ""
|
|
100
|
+
# Seed broadcast_pg_conn later via lib/tep.rb's setter seed
|
|
101
|
+
# (APP.set_broadcast_pg_conn(PG::Connection.new(""))) -- module
|
|
102
|
+
# load order means PG::Connection isn't safely callable from
|
|
103
|
+
# App#initialize when this is loaded before pg.rb's full surface.
|
|
104
|
+
@nf_handler = Handler.new
|
|
105
|
+
@asset_bodies = Tep.str_hash # path -> bytes (filled at boot
|
|
106
|
+
@asset_mimes = Tep.str_hash # by Tep::Assets._add lines
|
|
107
|
+
# the bin/tep translator emits)
|
|
108
|
+
@asset_etags = Tep.str_hash # path -> content-hash ETag (#152)
|
|
109
|
+
# FiberSlot array for the cooperative scheduler. Initialise
|
|
110
|
+
# with a noop-bodied slot to pin the array element type, then
|
|
111
|
+
# drop it. Each slot holds one Fiber + a timer entry in the
|
|
112
|
+
# parallel `sched_wake_at` int array.
|
|
113
|
+
@sched_fibers = [Tep::FiberSlot.new(Fiber.new { Tep.seed_fiber_noop })]
|
|
114
|
+
@sched_fibers.delete_at(0)
|
|
115
|
+
@sched_wake_at = [0]
|
|
116
|
+
@sched_wake_at.delete_at(0)
|
|
117
|
+
@sched_current = -1 # currently-running fiber idx
|
|
118
|
+
# (-1 = scheduler root).
|
|
119
|
+
# Parallel I/O-wait arrays. `sched_io_fd[i] == -1` means the
|
|
120
|
+
# fiber isn't parked on I/O (pure timer wait, or ready). When
|
|
121
|
+
# parked: `sched_io_mode[i]` carries the requested READ/WRITE
|
|
122
|
+
# bits, and tick() writes back the observed-ready bits into
|
|
123
|
+
# `sched_io_ready[i]`. io_wait returns those bits to its caller.
|
|
124
|
+
@sched_io_fd = [0]
|
|
125
|
+
@sched_io_fd.delete_at(0)
|
|
126
|
+
@sched_io_mode = [0]
|
|
127
|
+
@sched_io_mode.delete_at(0)
|
|
128
|
+
@sched_io_ready = [0]
|
|
129
|
+
@sched_io_ready.delete_at(0)
|
|
130
|
+
@user_bg_started = false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def add_asset(path, body, mime)
|
|
134
|
+
@asset_bodies[path] = body
|
|
135
|
+
@asset_mimes[path] = mime
|
|
136
|
+
# Content-hash ETag for cache revalidation (#152). SHA-1 is used
|
|
137
|
+
# purely as a fast content fingerprint here (not a security hash --
|
|
138
|
+
# collision resistance is irrelevant for an ETag, same as git's
|
|
139
|
+
# content addressing). Computed once at boot. (Binary bodies with
|
|
140
|
+
# embedded NULs hash by their leading bytes via the FFI string
|
|
141
|
+
# boundary; still stable per content, which is all an ETag needs.)
|
|
142
|
+
@asset_etags[path] = Crypto.sp_crypto_sha1_hex(body)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def set_session_secret(s)
|
|
146
|
+
@session_secret = s
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Inbound TLS cert/key paths (tep#148 phase 2). Set via
|
|
150
|
+
# Tep.tls_cert= / Tep.tls_key=; read by Tep::Server.run at boot.
|
|
151
|
+
def set_tls_cert(s)
|
|
152
|
+
@tls_cert = s
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def set_tls_key(s)
|
|
156
|
+
@tls_key = s
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def add_route(verb, pattern, handler)
|
|
160
|
+
@router.add(verb, pattern, handler)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def set_static_root(root); @static_root = root; end
|
|
164
|
+
def set_before(f); @before_filter = f; end
|
|
165
|
+
def set_after(f); @after_filter = f; end
|
|
166
|
+
def set_auth_filter(f); @auth_filter = f; end
|
|
167
|
+
def set_auth_bearer_secret(s); @auth_bearer_secret = s; end
|
|
168
|
+
def set_broadcast_pg_enabled(v); @broadcast_pg_enabled = v; end
|
|
169
|
+
def set_broadcast_pg_channel(s); @broadcast_pg_channel = s; end
|
|
170
|
+
def set_broadcast_pg_conn(c); @broadcast_pg_conn = c; end
|
|
171
|
+
def set_presence_pg_enabled(v); @presence_pg_enabled = v; end
|
|
172
|
+
def set_presence_pg_worker_id(s); @presence_pg_worker_id = s; end
|
|
173
|
+
def set_presence_pg_conn(c); @presence_pg_conn = c; end
|
|
174
|
+
def set_openai_backend(b); @openai_backend = b; end
|
|
175
|
+
def set_openai_events(e); @openai_events = e; end
|
|
176
|
+
def set_not_found(h); @nf_handler = h; end
|
|
177
|
+
|
|
178
|
+
def dispatch(req, res)
|
|
179
|
+
# Pull a signed session cookie into req.session, when configured.
|
|
180
|
+
secret = Tep.session_secret
|
|
181
|
+
if secret.length > 0
|
|
182
|
+
cv = req.cookies[Tep::COOKIE_NAME]
|
|
183
|
+
if cv.length > 0
|
|
184
|
+
req.session.load_from(cv, secret)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
asset_served = false
|
|
189
|
+
# Auth filter populates req.identity (anonymous or matched
|
|
190
|
+
# provider's Identity) before the user's before-filter runs,
|
|
191
|
+
# so user code can always rely on req.identity being set.
|
|
192
|
+
@auth_filter.before(req, res)
|
|
193
|
+
if res.halted
|
|
194
|
+
# Auth filter signalled "deny" -- skip the user filter +
|
|
195
|
+
# route dispatch, fall through to after-filter + session.
|
|
196
|
+
end
|
|
197
|
+
@before_filter.before(req, res)
|
|
198
|
+
if !res.halted
|
|
199
|
+
# Bundled assets (everything under <app>/assets/, baked into
|
|
200
|
+
# the binary by bin/tep) take precedence over the route
|
|
201
|
+
# table. Match by exact path; on hit we set the body + ct
|
|
202
|
+
# and skip route dispatch + 404 fallback. The after-filter
|
|
203
|
+
# and session cookie writing still run normally.
|
|
204
|
+
if Tep::Assets.serve(req.path, res)
|
|
205
|
+
asset_served = true
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
if !res.halted && !asset_served
|
|
209
|
+
route = @router.match(req)
|
|
210
|
+
# `pass` loop: a handler can signal skip-to-next-route by
|
|
211
|
+
# setting req.passed. Iterate until a handler doesn't pass,
|
|
212
|
+
# or we run out of matching routes.
|
|
213
|
+
served = false
|
|
214
|
+
while route != nil && !served
|
|
215
|
+
route.fold_captures(req)
|
|
216
|
+
req.passed = false
|
|
217
|
+
out = route.handler.handle(req, res)
|
|
218
|
+
if req.passed
|
|
219
|
+
idx = @router.index_of(route)
|
|
220
|
+
route = @router.match_after(req, idx)
|
|
221
|
+
else
|
|
222
|
+
res.set_body_if_empty(out)
|
|
223
|
+
served = true
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
if !served
|
|
227
|
+
if !try_static(req, res)
|
|
228
|
+
out = @nf_handler.handle(req, res)
|
|
229
|
+
res.set_status(404)
|
|
230
|
+
if out.length > 0
|
|
231
|
+
res.set_body_if_empty(out)
|
|
232
|
+
else
|
|
233
|
+
res.set_body_if_empty("<h1>404 Not Found</h1><p>" +
|
|
234
|
+
req.verb + " " + req.path + "</p>\n")
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
@after_filter.after(req, res)
|
|
240
|
+
|
|
241
|
+
# If the handler / filters mutated the session, sign + emit a
|
|
242
|
+
# Set-Cookie line. Path=/ so the cookie applies to the whole
|
|
243
|
+
# app; HttpOnly to keep it out of JS.
|
|
244
|
+
secret_w = Tep.session_secret
|
|
245
|
+
if secret_w.length > 0 && req.session.dirty
|
|
246
|
+
opts = Tep.str_hash
|
|
247
|
+
opts["Path"] = "/"
|
|
248
|
+
opts["HttpOnly"] = ""
|
|
249
|
+
opts["SameSite"] = "Lax"
|
|
250
|
+
res.set_cookie(Tep::COOKIE_NAME, req.session.to_cookie_value(secret_w), opts)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def try_static(req, res)
|
|
255
|
+
if @static_root.length == 0
|
|
256
|
+
return false
|
|
257
|
+
end
|
|
258
|
+
if req.verb != "GET" && req.verb != "HEAD"
|
|
259
|
+
return false
|
|
260
|
+
end
|
|
261
|
+
if Tep.str_find(req.path, "..", 0) >= 0
|
|
262
|
+
return false
|
|
263
|
+
end
|
|
264
|
+
full = @static_root + req.path
|
|
265
|
+
sz = Sock.sphttp_filesize(full)
|
|
266
|
+
if sz < 0
|
|
267
|
+
return false
|
|
268
|
+
end
|
|
269
|
+
res.headers["Content-Type"] = App.guess_mime(full)
|
|
270
|
+
res.headers["X-Tep-Static"] = "1"
|
|
271
|
+
res.send_file(full)
|
|
272
|
+
true
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def self.guess_mime(path)
|
|
276
|
+
lower = path.downcase
|
|
277
|
+
if lower.end_with?(".html") || lower.end_with?(".htm")
|
|
278
|
+
return "text/html; charset=utf-8"
|
|
279
|
+
end
|
|
280
|
+
if lower.end_with?(".css"); return "text/css"; end
|
|
281
|
+
if lower.end_with?(".js"); return "application/javascript"; end
|
|
282
|
+
if lower.end_with?(".json"); return "application/json"; end
|
|
283
|
+
if lower.end_with?(".png"); return "image/png"; end
|
|
284
|
+
if lower.end_with?(".jpg") || lower.end_with?(".jpeg"); return "image/jpeg"; end
|
|
285
|
+
if lower.end_with?(".gif"); return "image/gif"; end
|
|
286
|
+
if lower.end_with?(".svg"); return "image/svg+xml"; end
|
|
287
|
+
if lower.end_with?(".txt"); return "text/plain; charset=utf-8"; end
|
|
288
|
+
"application/octet-stream"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|