devbench 0.6.0 → 0.8.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 +4 -4
- data/LICENSE +21 -0
- data/README.md +83 -4
- data/lib/devbench/active_job_hooks.rb +121 -0
- data/lib/devbench/config.rb +49 -7
- data/lib/devbench/current.rb +49 -0
- data/lib/devbench/direct.rb +30 -4
- data/lib/devbench/identity.rb +17 -3
- data/lib/devbench/logs.rb +84 -13
- data/lib/devbench/middleware.rb +58 -2
- data/lib/devbench/rails_hooks.rb +1 -1
- data/lib/devbench/railtie.rb +7 -1
- data/lib/devbench/reporter.rb +17 -5
- data/lib/devbench/request_signals.rb +359 -0
- data/lib/devbench/scrub.rb +69 -3
- data/lib/devbench/session.rb +26 -5
- data/lib/devbench/sidecar_transport.rb +1 -1
- data/lib/devbench/sidekiq_hooks.rb +1 -1
- data/lib/devbench/trace.rb +1 -1
- data/lib/devbench/transport.rb +9 -1
- data/lib/devbench/version.rb +18 -6
- data/lib/devbench/view_helper.rb +24 -1
- data/lib/devbench/widget_identity.rb +125 -0
- data/lib/devbench.rb +5 -0
- metadata +7 -3
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
|
|
6
|
+
module Devbench
|
|
7
|
+
# The chat widget's signed identity (docs/server-sdk/chat-widget.md, "Capability 6
|
|
8
|
+
# — Chat widget").
|
|
9
|
+
#
|
|
10
|
+
# dbw1.<base64url(issued_at \n email \n account \n role \n chat)>.<base64url(HMAC-SHA256)>
|
|
11
|
+
#
|
|
12
|
+
# The app's server signs the signed-in user with the tenant's widget secret
|
|
13
|
+
# (DEVBENCH_WIDGET_SECRET); the browser holds only the token, and the chat
|
|
14
|
+
# service shows the widget only to a user whose token verifies. Every signer
|
|
15
|
+
# must reproduce testdata/widget_identity_vectors.json byte for byte, or the
|
|
16
|
+
# widget works in a unit test and never renders in production.
|
|
17
|
+
#
|
|
18
|
+
# Rails apps never call this: devbench_script_tag signs whatever
|
|
19
|
+
# Devbench.set_user recorded for the request.
|
|
20
|
+
module WidgetIdentity
|
|
21
|
+
PREFIX = 'dbw1'
|
|
22
|
+
# The closed list, in the token's canonical (byte) order. A new one is a
|
|
23
|
+
# contract change, made in lockstep with the vectors.
|
|
24
|
+
CAPABILITIES = %w[feature_requests handoff howto report status].freeze
|
|
25
|
+
MAX = 255
|
|
26
|
+
# ASCII whitespace only, as the contract names it. String#strip would
|
|
27
|
+
# also remove a trailing NUL, which must survive the trim to be refused
|
|
28
|
+
# (Go's signer refuses it): the two would disagree.
|
|
29
|
+
TRIM = /\A[ \t\n\r\f\v]+|[ \t\n\r\f\v]+\z/
|
|
30
|
+
# C0 controls and DEL, refused when left in a field after trimming (the
|
|
31
|
+
# trimmed whitespace is C0 too, and fine at the ends). Identities are
|
|
32
|
+
# compared and displayed; invisible bytes could make two look alike.
|
|
33
|
+
CONTROL = /[\x00-\x1f\x7f]/
|
|
34
|
+
NO_CHAT = 'none'
|
|
35
|
+
|
|
36
|
+
# Raised internally for an identity that cannot be signed; never escapes.
|
|
37
|
+
class Refused < StandardError; end
|
|
38
|
+
|
|
39
|
+
# Returns the token, or nil when this identity must not be signed: no
|
|
40
|
+
# secret, neither email nor account, a control character (U+0000-U+001F,
|
|
41
|
+
# U+007F) left in a field after trimming, a field over 255 characters,
|
|
42
|
+
# invalid text, or a chat capability not in CAPABILITIES (exact,
|
|
43
|
+
# lowercase). Refuses rather than repairs: truncating would sign a
|
|
44
|
+
# different person. The first refusal in the process is logged, once.
|
|
45
|
+
#
|
|
46
|
+
# chat: nil states nothing (the tenant's settings decide); [] means the
|
|
47
|
+
# user may not chat; otherwise their capabilities, symbols or strings,
|
|
48
|
+
# in any order, duplicates allowed. Never raises.
|
|
49
|
+
def self.sign(secret, email: nil, account: nil, role: nil, chat: nil, issued_at: Time.now.to_i)
|
|
50
|
+
token(secret.to_s, email, account, role, chat, issued_at)
|
|
51
|
+
rescue Refused => e
|
|
52
|
+
Devbench.warn_once(:widget_identity, "chat widget identity not signed: #{e.message}; " \
|
|
53
|
+
'no chat widget for that user (further refusals are not logged)')
|
|
54
|
+
nil
|
|
55
|
+
rescue StandardError, SystemStackError
|
|
56
|
+
Devbench.warn_once(:widget_identity, 'chat widget identity not signed: unexpected error; ' \
|
|
57
|
+
'no chat widget for that user (further refusals are not logged)')
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.token(secret, email, account, role, chat, issued_at)
|
|
62
|
+
raise Refused, 'no widget secret (DEVBENCH_WIDGET_SECRET)' if secret.empty?
|
|
63
|
+
|
|
64
|
+
email = field(email, 'email')
|
|
65
|
+
account = field(account, 'account')
|
|
66
|
+
role = field(role, 'role')
|
|
67
|
+
# Simple lowercase mapping, as Go's strings.ToLower: Ruby's full
|
|
68
|
+
# mapping turns U+0130 into two characters where Go makes one. It is
|
|
69
|
+
# the only code point the two disagree on.
|
|
70
|
+
email = email.tr("\u0130", 'i').downcase
|
|
71
|
+
raise Refused, 'neither email nor account' if email.empty? && account.empty?
|
|
72
|
+
|
|
73
|
+
ts = Integer(issued_at)
|
|
74
|
+
raise Refused, 'issued before 1970' if ts.negative?
|
|
75
|
+
|
|
76
|
+
raw = [ts.to_s, email, account, role, chat_field(chat)].join("\n")
|
|
77
|
+
payload = b64(raw.b)
|
|
78
|
+
"#{PREFIX}.#{payload}.#{b64(OpenSSL::HMAC.digest('SHA256', secret.b, "#{PREFIX}.#{payload}"))}"
|
|
79
|
+
end
|
|
80
|
+
private_class_method :token
|
|
81
|
+
|
|
82
|
+
# Trimmed, valid UTF-8, no control character, at most MAX characters; else
|
|
83
|
+
# Refused. nil is "".
|
|
84
|
+
def self.field(value, name)
|
|
85
|
+
text = utf8(value, name).gsub(TRIM, '')
|
|
86
|
+
raise Refused, "control character in #{name}" if text.match?(CONTROL)
|
|
87
|
+
raise Refused, "#{name} over #{MAX} characters" if text.length > MAX
|
|
88
|
+
|
|
89
|
+
text
|
|
90
|
+
end
|
|
91
|
+
private_class_method :field
|
|
92
|
+
|
|
93
|
+
def self.utf8(value, name)
|
|
94
|
+
text = value.nil? ? +'' : value.to_s.dup
|
|
95
|
+
text = text.encoding == Encoding::BINARY ? text.force_encoding(Encoding::UTF_8) : text.encode(Encoding::UTF_8)
|
|
96
|
+
raise Refused, "#{name} is not valid UTF-8" unless text.valid_encoding?
|
|
97
|
+
|
|
98
|
+
text
|
|
99
|
+
rescue EncodingError
|
|
100
|
+
raise Refused, "#{name} is not valid UTF-8"
|
|
101
|
+
end
|
|
102
|
+
private_class_method :utf8
|
|
103
|
+
|
|
104
|
+
# "" not stated, "none" may not chat, else known names de-duplicated,
|
|
105
|
+
# sorted by bytes, joined by ",".
|
|
106
|
+
def self.chat_field(chat)
|
|
107
|
+
return '' if chat.nil?
|
|
108
|
+
raise Refused, 'chat must be a list of capabilities' unless chat.is_a?(Array)
|
|
109
|
+
|
|
110
|
+
names = chat.map do |c|
|
|
111
|
+
name = c.is_a?(Symbol) || c.is_a?(String) ? c.to_s : nil
|
|
112
|
+
raise Refused, "unknown chat capability #{c.inspect}" unless CAPABILITIES.include?(name)
|
|
113
|
+
|
|
114
|
+
name
|
|
115
|
+
end
|
|
116
|
+
names.empty? ? NO_CHAT : names.uniq.sort.join(',')
|
|
117
|
+
end
|
|
118
|
+
private_class_method :chat_field
|
|
119
|
+
|
|
120
|
+
def self.b64(bytes)
|
|
121
|
+
Base64.urlsafe_encode64(bytes, padding: false)
|
|
122
|
+
end
|
|
123
|
+
private_class_method :b64
|
|
124
|
+
end
|
|
125
|
+
end
|
data/lib/devbench.rb
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
# * reports unhandled exceptions (Devbench::Middleware, Devbench::Railtie,
|
|
14
14
|
# Devbench::SidekiqHooks,
|
|
15
15
|
# Devbench.capture_exception)
|
|
16
|
+
# * reports failed (5xx) and slow requests (Devbench::Middleware,
|
|
17
|
+
# and 429 spikes Devbench::RequestSignals)
|
|
16
18
|
# * keeps recent log lines per trace (Devbench::Logs; direct mode)
|
|
17
19
|
# * renders the browser sensor's tag (devbench_script_tag)
|
|
18
20
|
#
|
|
@@ -23,6 +25,7 @@ require_relative 'devbench/trace'
|
|
|
23
25
|
require_relative 'devbench/current'
|
|
24
26
|
require_relative 'devbench/backtrace'
|
|
25
27
|
require_relative 'devbench/identity'
|
|
28
|
+
require_relative 'devbench/request_signals'
|
|
26
29
|
require_relative 'devbench/middleware'
|
|
27
30
|
require_relative 'devbench/http'
|
|
28
31
|
require_relative 'devbench/reporter'
|
|
@@ -30,7 +33,9 @@ require_relative 'devbench/session'
|
|
|
30
33
|
require_relative 'devbench/session_middleware'
|
|
31
34
|
require_relative 'devbench/rails_hooks'
|
|
32
35
|
require_relative 'devbench/sidekiq_hooks'
|
|
36
|
+
require_relative 'devbench/active_job_hooks'
|
|
33
37
|
require_relative 'devbench/logs'
|
|
38
|
+
require_relative 'devbench/widget_identity'
|
|
34
39
|
require_relative 'devbench/view_helper'
|
|
35
40
|
|
|
36
41
|
# The pre-0.5 name. The same module object, not a copy: ADT::Middleware *is*
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: devbench
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dev Bench
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Reports unhandled exceptions, Rails.error reports, failed ActiveJob and
|
|
@@ -27,9 +27,11 @@ executables: []
|
|
|
27
27
|
extensions: []
|
|
28
28
|
extra_rdoc_files: []
|
|
29
29
|
files:
|
|
30
|
+
- LICENSE
|
|
30
31
|
- README.md
|
|
31
32
|
- lib/adt.rb
|
|
32
33
|
- lib/devbench.rb
|
|
34
|
+
- lib/devbench/active_job_hooks.rb
|
|
33
35
|
- lib/devbench/backtrace.rb
|
|
34
36
|
- lib/devbench/config.rb
|
|
35
37
|
- lib/devbench/current.rb
|
|
@@ -43,6 +45,7 @@ files:
|
|
|
43
45
|
- lib/devbench/rails_hooks.rb
|
|
44
46
|
- lib/devbench/railtie.rb
|
|
45
47
|
- lib/devbench/reporter.rb
|
|
48
|
+
- lib/devbench/request_signals.rb
|
|
46
49
|
- lib/devbench/scrub.rb
|
|
47
50
|
- lib/devbench/session.rb
|
|
48
51
|
- lib/devbench/session_middleware.rb
|
|
@@ -52,10 +55,11 @@ files:
|
|
|
52
55
|
- lib/devbench/transport.rb
|
|
53
56
|
- lib/devbench/version.rb
|
|
54
57
|
- lib/devbench/view_helper.rb
|
|
58
|
+
- lib/devbench/widget_identity.rb
|
|
55
59
|
- lib/generators/devbench/devbench_generator.rb
|
|
56
60
|
homepage: https://github.com/pasperry/devbench-sdk
|
|
57
61
|
licenses:
|
|
58
|
-
-
|
|
62
|
+
- MIT
|
|
59
63
|
metadata:
|
|
60
64
|
rubygems_mfa_required: 'true'
|
|
61
65
|
source_code_uri: https://github.com/pasperry/devbench-sdk
|