tep 0.11.3 → 0.11.5

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/Makefile +42 -2
  3. data/README.md +4 -4
  4. data/SINATRA_COMPAT.md +20 -20
  5. data/bin/tep +47 -10
  6. data/examples/api_gateway/app.rb +1 -1
  7. data/examples/blog/app.rb +17 -17
  8. data/examples/chat/app.rb +12 -12
  9. data/examples/chatbot/README.md +2 -2
  10. data/examples/chatbot/app.rb +24 -24
  11. data/examples/llm_gateway/app.rb +4 -4
  12. data/examples/pg_hello.rb +11 -1
  13. data/lib/spinel_kit/hex.rb +65 -0
  14. data/lib/spinel_kit/json.rb +151 -0
  15. data/lib/spinel_kit/json_decoder.rb +396 -0
  16. data/lib/{tep/logger.rb → spinel_kit/log.rb} +25 -21
  17. data/lib/spinel_kit/url.rb +166 -0
  18. data/lib/tep/auth_bearer_token.rb +6 -6
  19. data/lib/tep/auth_oauth2.rb +4 -4
  20. data/lib/tep/broadcast.rb +18 -80
  21. data/lib/tep/events.rb +37 -37
  22. data/lib/tep/http.rb +3 -3
  23. data/lib/tep/job.rb +2 -2
  24. data/lib/tep/jwt.rb +4 -4
  25. data/lib/tep/live_view.rb +4 -4
  26. data/lib/tep/llm.rb +13 -45
  27. data/lib/tep/mcp.rb +12 -12
  28. data/lib/tep/multipart.rb +1 -1
  29. data/lib/tep/net.rb +8 -3
  30. data/lib/tep/openai_server.rb +102 -94
  31. data/lib/tep/parser.rb +2 -2
  32. data/lib/tep/pg.rb +468 -14
  33. data/lib/tep/presence.rb +33 -329
  34. data/lib/tep/proxy.rb +7 -7
  35. data/lib/tep/request.rb +1 -1
  36. data/lib/tep/response.rb +1 -1
  37. data/lib/tep/router.rb +1 -1
  38. data/lib/tep/session.rb +2 -2
  39. data/lib/tep/version.rb +1 -1
  40. data/lib/tep.rb +57 -137
  41. data/spinel-ext.json +6 -0
  42. data/test/helper.rb +95 -8
  43. data/test/run_parallel.rb +44 -7
  44. data/test/test_auth.rb +17 -17
  45. data/test/test_auth_oauth2.rb +5 -5
  46. data/test/test_broadcast_pg.rb +1 -0
  47. data/test/test_http_pool.rb +4 -4
  48. data/test/test_http_pool_send.rb +3 -3
  49. data/test/test_json.rb +12 -12
  50. data/test/test_jwt.rb +4 -4
  51. data/test/test_live_view.rb +3 -3
  52. data/test/test_llm.rb +12 -9
  53. data/test/test_llm_gateway.rb +2 -2
  54. data/test/test_logger.rb +2 -2
  55. data/test/test_openai_server.rb +10 -1
  56. data/test/test_password.rb +3 -3
  57. data/test/test_pg.rb +1 -0
  58. data/test/test_presence_pg.rb +1 -0
  59. data/test/test_real_world.rb +6 -1
  60. data/test/test_shutdown.rb +40 -0
  61. metadata +23 -8
  62. data/lib/tep/json.rb +0 -572
  63. data/lib/tep/url.rb +0 -161
data/lib/tep/presence.rb CHANGED
@@ -211,21 +211,21 @@ module Tep
211
211
  end
212
212
 
213
213
  # Flat-JSON wire format for a diff event. `kind` is one of
214
- # "join" / "leave" / "status". Tep::Json's flat-object
214
+ # "join" / "leave" / "status". SpinelKit::Json's flat-object
215
215
  # extractors handle this on the client side (or any
216
216
  # JSON-aware peer).
217
217
  def self.encode_diff(kind, entry)
218
218
  "{" +
219
- Tep::Json.encode_pair_str("kind", kind) + "," +
220
- Tep::Json.encode_pair_str("topic", entry.topic) + "," +
221
- Tep::Json.encode_pair_str("principal", entry.principal_id) + "," +
222
- Tep::Json.encode_pair_str("ekind", entry.kind.to_s) + "," +
223
- Tep::Json.encode_pair_str("agent_id", entry.agent_id) + "," +
224
- Tep::Json.encode_pair_int("fd", entry.fd) + "," +
225
- Tep::Json.encode_pair_int("since", entry.since) + "," +
226
- Tep::Json.encode_pair_str("state", entry.status_state.to_s) + "," +
227
- Tep::Json.encode_pair_str("note", entry.status_note) + "," +
228
- Tep::Json.encode_pair_int("until_ts", entry.status_until) +
219
+ SpinelKit::Json.encode_pair_str("kind", kind) + "," +
220
+ SpinelKit::Json.encode_pair_str("topic", entry.topic) + "," +
221
+ SpinelKit::Json.encode_pair_str("principal", entry.principal_id) + "," +
222
+ SpinelKit::Json.encode_pair_str("ekind", entry.kind.to_s) + "," +
223
+ SpinelKit::Json.encode_pair_str("agent_id", entry.agent_id) + "," +
224
+ SpinelKit::Json.encode_pair_int("fd", entry.fd) + "," +
225
+ SpinelKit::Json.encode_pair_int("since", entry.since) + "," +
226
+ SpinelKit::Json.encode_pair_str("state", entry.status_state.to_s) + "," +
227
+ SpinelKit::Json.encode_pair_str("note", entry.status_note) + "," +
228
+ SpinelKit::Json.encode_pair_int("until_ts", entry.status_until) +
229
229
  "}"
230
230
  end
231
231
 
@@ -265,325 +265,29 @@ module Tep
265
265
  swept
266
266
  end
267
267
 
268
- # ---- PG mirror (cross-worker visibility) ----
269
- #
270
- # Opt-in mirror of the local presence registry to a shared PG
271
- # table. Each worker's track/untrack/set_status writes also
272
- # touch the table; list_global / count_global read across all
273
- # workers. The local registry stays the fast read path for
274
- # per-worker queries (list / count); list_global is for the
275
- # "who's globally in this room" snapshot that's typically a
276
- # one-shot UI render.
277
- #
278
- # Worker ID is PID + boot epoch second so a same-PID restart
279
- # doesn't alias a prior worker's stale rows. On
280
- # disable_pg_mirror (or clean shutdown), this worker's rows
281
- # get DELETE'd. Crashed workers leave stale rows; the
282
- # heartbeat + prune_stale_workers pair below handles the
283
- # garbage-collection.
284
- #
285
- # Returns 0 on success, -1 on connect / schema failure.
286
- def self.enable_pg_mirror(conninfo)
287
- conn = PG::Connection.new(conninfo)
288
- if conn.pgh < 0
289
- return -1
290
- end
291
- # exec raises PG::Error on failure now; degrade gracefully
292
- # (close + return -1) rather than letting it escape the worker.
293
- begin
294
- r = conn.exec(Tep::Presence.schema_sql)
295
- r.clear
296
- # Heartbeat table for the prune-stale-workers path (#47).
297
- r = conn.exec(Tep::Presence.worker_schema_sql)
298
- r.clear
299
- rescue PG::Error
300
- conn.finish
301
- return -1
302
- end
303
- Tep::APP.set_presence_pg_conn(conn)
304
- worker_id = Sock.sphttp_getpid.to_s + "-" + Time.now.to_i.to_s
305
- Tep::APP.set_presence_pg_worker_id(worker_id)
306
- Tep::APP.set_presence_pg_enabled(1)
307
- # Drop any rows from a prior worker that managed to leave
308
- # stale entries with this same worker_id (unlikely thanks
309
- # to the boot-epoch suffix, but defensive). Best-effort.
310
- Tep::Presence.mirror_exec(
311
- "DELETE FROM tep_presence WHERE worker_id = $1",
312
- [worker_id])
313
- # Register this worker's heartbeat row immediately. Apps
314
- # refresh it periodically via Tep::Presence.heartbeat;
315
- # prune_stale_workers deletes rows whose heartbeat is stale.
316
- Tep::Presence.heartbeat
317
- 0
318
- end
319
-
320
- def self.disable_pg_mirror
321
- if Tep::APP.presence_pg_enabled == 0
322
- return 0
323
- end
324
- # Best-effort cleanup -- swallow PG errors (we're tearing the
325
- # mirror down regardless) and still finish + disable below.
326
- begin
327
- r = Tep::APP.presence_pg_conn.exec_params(
328
- "DELETE FROM tep_presence WHERE worker_id = $1",
329
- [Tep::APP.presence_pg_worker_id])
330
- r.clear
331
- # Remove the heartbeat row so prune_stale_workers doesn't
332
- # see this worker as live after we're gone.
333
- r = Tep::APP.presence_pg_conn.exec_params(
334
- "DELETE FROM tep_presence_worker WHERE worker_id = $1",
335
- [Tep::APP.presence_pg_worker_id])
336
- r.clear
337
- rescue PG::Error
338
- # swallow -- shutting the mirror down anyway
339
- end
340
- Tep::APP.presence_pg_conn.finish
341
- Tep::APP.set_presence_pg_enabled(0)
342
- 0
343
- end
344
-
345
- # CREATE TABLE statement, kept here so apps that want to
346
- # provision the schema separately (migration runners, etc.)
347
- # can grab the canonical DDL. Idempotent via IF NOT EXISTS.
348
- def self.schema_sql
349
- "CREATE TABLE IF NOT EXISTS tep_presence (" +
350
- "worker_id TEXT NOT NULL, " +
351
- "topic TEXT NOT NULL, " +
352
- "fd INTEGER NOT NULL, " +
353
- "principal_id TEXT NOT NULL, " +
354
- "kind TEXT NOT NULL, " +
355
- "agent_id TEXT NOT NULL, " +
356
- "since_ts BIGINT NOT NULL, " +
357
- "status_state TEXT NOT NULL, " +
358
- "status_note TEXT NOT NULL, " +
359
- "status_until BIGINT NOT NULL, " +
360
- "PRIMARY KEY (worker_id, topic, fd)" +
361
- ")"
362
- end
363
-
364
- # Heartbeat table -- one row per worker that's mirroring
365
- # presence right now. Used by prune_stale_workers to identify
366
- # crashed workers (no heartbeat updates in N seconds) and
367
- # garbage-collect their orphan tep_presence rows.
368
- def self.worker_schema_sql
369
- "CREATE TABLE IF NOT EXISTS tep_presence_worker (" +
370
- "worker_id TEXT PRIMARY KEY, " +
371
- "last_seen_ts BIGINT NOT NULL" +
372
- ")"
373
- end
374
-
375
- # Refresh this worker's heartbeat row to the current Unix
376
- # timestamp. Apps call this periodically (typical: from a
377
- # before-filter, a Tep::Job tick, or an explicit timer fiber)
378
- # so prune_stale_workers can tell live workers from crashed
379
- # ones. No-op when the PG mirror isn't enabled, or when the
380
- # mirror was opened on a different process and we're the
381
- # post-fork child (worker_id is empty until enable_pg_mirror
382
- # runs locally).
383
- #
384
- # Returns 1 if the heartbeat row was upserted, 0 if the call
385
- # short-circuited (mirror disabled or no worker_id).
386
- def self.heartbeat
387
- if Tep::APP.presence_pg_enabled == 0
388
- return 0
389
- end
390
- wid = Tep::APP.presence_pg_worker_id
391
- if wid.length == 0
392
- return 0
393
- end
394
- begin
395
- r = Tep::APP.presence_pg_conn.exec_params(
396
- "INSERT INTO tep_presence_worker (worker_id, last_seen_ts) " +
397
- "VALUES ($1, $2) " +
398
- "ON CONFLICT (worker_id) DO UPDATE SET " +
399
- " last_seen_ts = EXCLUDED.last_seen_ts",
400
- [wid, Time.now.to_i.to_s])
401
- r.clear
402
- rescue PG::Error
403
- return 0
404
- end
405
- 1
406
- end
407
-
408
- # Prune crashed-worker rows. Deletes:
409
- # 1. tep_presence_worker rows whose last_seen_ts is older than
410
- # ttl_seconds (the worker's heartbeat is stale).
411
- # 2. tep_presence rows whose worker_id has no surviving
412
- # heartbeat (orphans left by the crashed worker).
413
- #
414
- # Apps call this periodically -- the canonical shape is a
415
- # before-filter on a "/health" route that internal monitoring
416
- # hits every 30s, or a Tep::Job that fires from a cron-like
417
- # tick. Returns the number of tep_presence rows deleted.
418
- #
419
- # ttl_seconds should be at least 3x the app's typical
420
- # heartbeat interval so a transient slow response doesn't
421
- # evict a live worker. Default callers pass 90 (assumes 30s
422
- # heartbeats).
423
- def self.prune_stale_workers(ttl_seconds)
424
- if Tep::APP.presence_pg_enabled == 0
425
- return 0
426
- end
427
- cutoff = Time.now.to_i - ttl_seconds
428
- conn = Tep::APP.presence_pg_conn
429
- begin
430
- # Drop dead heartbeats first; the second DELETE then walks
431
- # the worker_id space that's still alive.
432
- r1 = conn.exec_params(
433
- "DELETE FROM tep_presence_worker WHERE last_seen_ts < $1",
434
- [cutoff.to_s])
435
- r1.clear
436
- # Now drop presence rows whose worker_id isn't in the live
437
- # heartbeat table. NOT IN handles both crashed-and-pruned
438
- # workers and workers that never registered (legacy rows
439
- # from before this prune feature shipped).
440
- r2 = conn.exec(
441
- "DELETE FROM tep_presence " +
442
- "WHERE worker_id NOT IN (SELECT worker_id FROM tep_presence_worker)")
443
- n = r2.cmd_tuples
444
- r2.clear
445
- rescue PG::Error
446
- return 0
447
- end
448
- n
449
- end
450
-
451
- # Best-effort mirror write: run an exec_params on the mirror conn
452
- # and swallow any PG::Error. The PG mirror is advisory -- local
453
- # presence is authoritative -- so a transient mirror failure must
454
- # never propagate into the caller's request now that exec raises
455
- # (matz/spinel#627 + #1041). Always returns 0.
456
- def self.mirror_exec(sql, params)
457
- begin
458
- r = Tep::APP.presence_pg_conn.exec_params(sql, params)
459
- r.clear
460
- rescue PG::Error
461
- # swallow -- advisory mirror, local presence is authoritative
462
- end
463
- 0
464
- end
465
-
466
- # Mirror a track to PG. Called from track() when the PG
467
- # mirror is enabled.
468
- def self.mirror_insert(entry)
469
- if Tep::APP.presence_pg_enabled == 0
470
- return 0
471
- end
472
- Tep::Presence.mirror_exec(
473
- "INSERT INTO tep_presence " +
474
- "(worker_id, topic, fd, principal_id, kind, agent_id, " +
475
- " since_ts, status_state, status_note, status_until) " +
476
- "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) " +
477
- "ON CONFLICT (worker_id, topic, fd) DO UPDATE SET " +
478
- " principal_id = EXCLUDED.principal_id, " +
479
- " kind = EXCLUDED.kind, " +
480
- " agent_id = EXCLUDED.agent_id, " +
481
- " since_ts = EXCLUDED.since_ts, " +
482
- " status_state = EXCLUDED.status_state, " +
483
- " status_note = EXCLUDED.status_note, " +
484
- " status_until = EXCLUDED.status_until",
485
- [
486
- Tep::APP.presence_pg_worker_id,
487
- entry.topic,
488
- entry.fd.to_s,
489
- entry.principal_id,
490
- entry.kind.to_s,
491
- entry.agent_id,
492
- entry.since.to_s,
493
- entry.status_state.to_s,
494
- entry.status_note,
495
- entry.status_until.to_s
496
- ])
497
- end
498
-
499
- # Mirror an untrack to PG.
500
- def self.mirror_delete(topic, fd)
501
- if Tep::APP.presence_pg_enabled == 0
502
- return 0
503
- end
504
- Tep::Presence.mirror_exec(
505
- "DELETE FROM tep_presence " +
506
- "WHERE worker_id = $1 AND topic = $2 AND fd = $3",
507
- [Tep::APP.presence_pg_worker_id, topic, fd.to_s])
508
- end
509
-
510
- # Mirror a status update.
511
- def self.mirror_status(topic, fd, state, note, until_ts)
512
- if Tep::APP.presence_pg_enabled == 0
513
- return 0
514
- end
515
- Tep::Presence.mirror_exec(
516
- "UPDATE tep_presence " +
517
- "SET status_state = $4, status_note = $5, status_until = $6 " +
518
- "WHERE worker_id = $1 AND topic = $2 AND fd = $3",
519
- [Tep::APP.presence_pg_worker_id, topic, fd.to_s,
520
- state.to_s, note, until_ts.to_s])
521
- end
268
+ # ---- PG mirror (cross-worker visibility) ----
269
+ #
270
+ # The presence PG mirror is OPT-IN (#216). enable_pg_mirror /
271
+ # disable_pg_mirror / heartbeat / prune_stale_workers / mirror_exec /
272
+ # list_global / count_global / the schema DDL, and the REAL bodies of
273
+ # the mirror_* hooks below, live in lib/tep/pg.rb and only compile
274
+ # into apps that `require "tep/pg"`. Core Presence keeps no PG
275
+ # reference, so a non-PG app DCEs the libpq closure entirely.
276
+ #
277
+ # track / untrack / set_status call these mirror_* hooks
278
+ # unconditionally; the core definitions are no-ops. `require
279
+ # "tep/pg"` redefines them (last-definition-wins) with the real
280
+ # exec_params writes.
281
+ def self.mirror_insert(entry)
282
+ 0
283
+ end
522
284
 
523
- # Cross-worker list: SELECT all entries on `topic` regardless
524
- # of which worker tracked them. Returns Array[PresenceEntry]
525
- # built from the PG rows. The returned entries are read-only
526
- # snapshots -- mutating them doesn't write back to PG.
527
- def self.list_global(topic)
528
- result = [Tep::PresenceEntry.new("", "", :human, "", -1, 0)]
529
- result.delete_at(0)
530
- if Tep::APP.presence_pg_enabled == 0
531
- return result
532
- end
533
- begin
534
- r = Tep::APP.presence_pg_conn.exec_params(
535
- "SELECT principal_id, kind, agent_id, fd, since_ts, " +
536
- " status_state, status_note, status_until " +
537
- "FROM tep_presence WHERE topic = $1 ORDER BY since_ts",
538
- [topic])
539
- rescue PG::Error
540
- return result
541
- end
542
- i = 0
543
- n = r.ntuples
544
- while i < n
545
- kind_sym = :human
546
- if r.getvalue(i, 1) == "agent_for"
547
- kind_sym = :agent_for
548
- end
549
- state_sym = :available
550
- sstr = r.getvalue(i, 5)
551
- if sstr == "busy"
552
- state_sym = :busy
553
- elsif sstr == "blocked"
554
- state_sym = :blocked
555
- end
556
- e = Tep::PresenceEntry.new(
557
- topic,
558
- r.getvalue(i, 0),
559
- kind_sym,
560
- r.getvalue(i, 2),
561
- r.getvalue(i, 3).to_i,
562
- r.getvalue(i, 4).to_i)
563
- e.status_state = state_sym
564
- e.status_note = r.getvalue(i, 6)
565
- e.status_until = r.getvalue(i, 7).to_i
566
- result.push(e)
567
- i += 1
568
- end
569
- r.clear
570
- result
571
- end
285
+ def self.mirror_delete(topic, fd)
286
+ 0
287
+ end
572
288
 
573
- def self.count_global(topic)
574
- if Tep::APP.presence_pg_enabled == 0
575
- return 0
576
- end
577
- begin
578
- r = Tep::APP.presence_pg_conn.exec_params(
579
- "SELECT count(*) FROM tep_presence WHERE topic = $1",
580
- [topic])
581
- rescue PG::Error
582
- return 0
583
- end
584
- n = r.getvalue(0, 0).to_i
585
- r.clear
586
- n
587
- end
289
+ def self.mirror_status(topic, fd, state, note, until_ts)
290
+ 0
291
+ end
588
292
  end
589
293
  end
data/lib/tep/proxy.rb CHANGED
@@ -12,7 +12,7 @@
12
12
  # end
13
13
  #
14
14
  # def after_forward(req, ures, res)
15
- # Tep::Logger.info("upstream " + ures.status.to_s)
15
+ # LOGGER.info("upstream " + ures.status.to_s) # LOGGER = SpinelKit::Log.new
16
16
  # 0
17
17
  # end
18
18
  # end
@@ -246,7 +246,7 @@ module Tep
246
246
  # An LLM gateway typically overrides this as:
247
247
  #
248
248
  # def stream_request?(req)
249
- # Tep::Json.get_bool(req.raw_body, "stream")
249
+ # SpinelKit::Json.get_bool(req.raw_body, "stream")
250
250
  # end
251
251
  def stream_request?(req)
252
252
  false
@@ -298,10 +298,10 @@ module Tep
298
298
  res.set_status(413)
299
299
  res.headers["Content-Type"] = "application/json"
300
300
  err_body = "{\"error\":{" +
301
- Tep::Json.encode_pair_str("message",
301
+ SpinelKit::Json.encode_pair_str("message",
302
302
  "request body exceeds proxy cap of " +
303
303
  @max_request_body_bytes.to_s + " bytes") + "," +
304
- Tep::Json.encode_pair_str("type", "payload_too_large") +
304
+ SpinelKit::Json.encode_pair_str("type", "payload_too_large") +
305
305
  "}}"
306
306
  res.set_body(err_body)
307
307
  return err_body
@@ -378,10 +378,10 @@ module Tep
378
378
  res.set_status(502)
379
379
  res.headers["Content-Type"] = "application/json"
380
380
  err_body = "{\"error\":{" +
381
- Tep::Json.encode_pair_str("message",
381
+ SpinelKit::Json.encode_pair_str("message",
382
382
  "upstream response body exceeds proxy cap of " +
383
383
  @max_response_body_bytes.to_s + " bytes") + "," +
384
- Tep::Json.encode_pair_str("type", "upstream_body_too_large") +
384
+ SpinelKit::Json.encode_pair_str("type", "upstream_body_too_large") +
385
385
  "}}"
386
386
  res.set_body(err_body)
387
387
  return err_body
@@ -424,7 +424,7 @@ module Tep
424
424
  # 502 and returns "" without starting a stream.
425
425
  def start_streaming_forward(req, res, ureq)
426
426
  url = pick_upstream(req) + ureq.path
427
- parts = Tep::Url.split_url(url)
427
+ parts = SpinelKit::Url.split_url(url)
428
428
  if parts["scheme"] != "http"
429
429
  res.set_status(502)
430
430
  return ""
data/lib/tep/request.rb CHANGED
@@ -180,7 +180,7 @@ module Tep
180
180
  # @raw_body to Content-Length.
181
181
  def parse_form_body
182
182
  if form?
183
- Url.parse_query(@raw_body).each do |k, v|
183
+ SpinelKit::Url.parse_query(@raw_body).each do |k, v|
184
184
  @params[k] = v
185
185
  end
186
186
  elsif multipart?
data/lib/tep/response.rb CHANGED
@@ -88,7 +88,7 @@ module Tep
88
88
  # (path, expires, max-age, domain, samesite, httponly, secure).
89
89
  # Empty `opts` is fine: just writes "name=value".
90
90
  def set_cookie(name, value, opts)
91
- line = name + "=" + Url.escape(value)
91
+ line = name + "=" + SpinelKit::Url.escape(value)
92
92
  if opts.length > 0
93
93
  opts.each do |k, v|
94
94
  if v.length == 0
data/lib/tep/router.rb CHANGED
@@ -77,7 +77,7 @@ module Tep
77
77
  pp = pat[i]
78
78
  if pp.length > 0 && pp[0] == ":"
79
79
  name = @r_params[pi]
80
- req.params[name] = Url.unescape(rp[i])
80
+ req.params[name] = SpinelKit::Url.unescape(rp[i])
81
81
  pi += 1
82
82
  end
83
83
  i += 1
data/lib/tep/session.rb CHANGED
@@ -46,7 +46,7 @@ module Tep
46
46
  if !Tep.timing_safe_eq(sig, expect)
47
47
  return false
48
48
  end
49
- Url.parse_query(payload).each do |k, v|
49
+ SpinelKit::Url.parse_query(payload).each do |k, v|
50
50
  @data[k] = v
51
51
  end
52
52
  true
@@ -61,7 +61,7 @@ module Tep
61
61
  if !first
62
62
  payload = payload + "&"
63
63
  end
64
- payload = payload + Url.escape(k) + "=" + Url.escape(v)
64
+ payload = payload + SpinelKit::Url.escape(k) + "=" + SpinelKit::Url.escape(v)
65
65
  first = false
66
66
  end
67
67
  payload + "." + Crypto.sp_crypto_hmac_sha256_hex(secret, payload)
data/lib/tep/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Tep
2
- VERSION = "0.11.3"
2
+ VERSION = "0.11.5"
3
3
  end