crudjt 1.0.0.pre.beta.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/.github/FUNDING.yml +15 -0
- data/.github/workflows/build.yml +41 -0
- data/.gitignore +8 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +102 -0
- data/LICENSE.txt +21 -0
- data/README.md +212 -0
- data/Rakefile +2 -0
- data/autotest.rb +192 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/crudjt.gemspec +44 -0
- data/lib/crudjt/version.rb +3 -0
- data/lib/crudjt.rb +260 -0
- data/lib/errors/internal_error.rb +5 -0
- data/lib/errors/invalid_state.rb +5 -0
- data/lib/errors.rb +7 -0
- data/lib/generated/token_service_pb.rb +22 -0
- data/lib/generated/token_service_services_pb.rb +25 -0
- data/lib/lru_cache.rb +77 -0
- data/lib/native/linux/store_jt_arm64.so +0 -0
- data/lib/native/linux/store_jt_x86_64.so +0 -0
- data/lib/native/macos/store_jt_arm64.dylib +0 -0
- data/lib/native/macos/store_jt_x86_64.dylib +0 -0
- data/lib/native/windows/store_jt_arm64.dll +0 -0
- data/lib/native/windows/store_jt_x86_64.dll +0 -0
- data/lib/token_service.proto +47 -0
- data/lib/token_service_impl.rb +57 -0
- data/lib/validation.rb +56 -0
- data/logos/buy_me_a_coffee_orange.svg +1 -0
- data/logos/crudjt_favicon_160x160_dark_on_white.svg +18 -0
- data/logos/crudjt_favicon_160x160_white.png +0 -0
- data/logos/crudjt_favicon_160x160_white_on_dark.svg +18 -0
- data/logos/crudjt_favicon_white_on_dark.png +0 -0
- data/logos/crudjt_logo_dark.png +0 -0
- data/logos/crudjt_logo_dark_on_white.svg +19 -0
- data/logos/crudjt_logo_white_on_dark.svg +26 -0
- metadata +146 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3b39e1babc7abf1c23c786365127925435747b9819cad1b8911f2f98629a97e5
|
|
4
|
+
data.tar.gz: 2c092c94d52ac011696d889aa31415c9eb82118ad26d5ac688c46b1686e9e1ea
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ee52b8fdec0f35f41b0353e11775e6e97504af1a4ac828f7c88310ac88b35f5f89bfd9e2d86773b63b14cc5317e4d2a8ce46b0ee872f135a0ef254a4df9daa5e
|
|
7
|
+
data.tar.gz: 584c45b5df3b5920c2e40177dd336524196c66047b9e0416e216d66f1d19dbe3c4f5f9467ed951f4ada0fa6d31fd4699e691dd5475a5931957a194e472775061
|
data/.github/FUNDING.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# These are supported funding model platforms
|
|
2
|
+
|
|
3
|
+
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
|
4
|
+
patreon: crudjt # Replace with a single Patreon username
|
|
5
|
+
open_collective: # Replace with a single Open Collective username
|
|
6
|
+
ko_fi: # Replace with a single Ko-fi username
|
|
7
|
+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
8
|
+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
9
|
+
liberapay: # Replace with a single Liberapay username
|
|
10
|
+
issuehunt: # Replace with a single IssueHunt username
|
|
11
|
+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
12
|
+
polar: # Replace with a single Polar username
|
|
13
|
+
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
|
14
|
+
thanks_dev: # Replace with a single thanks.dev username
|
|
15
|
+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Run autotest on multiple OS and architectures
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
test:
|
|
8
|
+
strategy:
|
|
9
|
+
matrix:
|
|
10
|
+
include:
|
|
11
|
+
- os: macos-latest
|
|
12
|
+
arch: aarch64
|
|
13
|
+
- os: ubuntu-latest
|
|
14
|
+
arch: x86_64
|
|
15
|
+
|
|
16
|
+
runs-on: ${{ matrix.os }}
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout code
|
|
20
|
+
uses: actions/checkout@v3
|
|
21
|
+
|
|
22
|
+
- name: Setup Ruby
|
|
23
|
+
uses: ruby/setup-ruby@v1
|
|
24
|
+
with:
|
|
25
|
+
ruby-version: '3.3'
|
|
26
|
+
bundler-cache: true
|
|
27
|
+
cache-version: 2
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies and build gem
|
|
30
|
+
run: |
|
|
31
|
+
gem install bundler --no-document
|
|
32
|
+
bundle install
|
|
33
|
+
bundle exec rake build
|
|
34
|
+
|
|
35
|
+
- name: Install gem
|
|
36
|
+
run: gem install pkg/crudjt-1.0.0-beta.0.gem --force
|
|
37
|
+
|
|
38
|
+
- name: Run autotest
|
|
39
|
+
env:
|
|
40
|
+
CRUDJT_AUTOTEST_ALLOWED: true
|
|
41
|
+
run: ruby autotest.rb
|
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
- Docker support
|
|
5
|
+
- Performance metrics
|
|
6
|
+
- Disk footprint
|
|
7
|
+
|
|
8
|
+
## [1.0.0-beta.0] - 2026-03-18
|
|
9
|
+
- Initial beta release
|
|
10
|
+
- Fast B-tree–backed token store for stateful user sessions
|
|
11
|
+
- Provides authentication and authorization across multiple processes
|
|
12
|
+
- Optimized for vertical scaling on a single server
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
crudjt (1.0.0.pre.beta.0)
|
|
5
|
+
ffi (~> 1.17)
|
|
6
|
+
grpc (~> 1.78.0)
|
|
7
|
+
lru_redux (~> 1.1)
|
|
8
|
+
msgpack (~> 1.8)
|
|
9
|
+
|
|
10
|
+
GEM
|
|
11
|
+
remote: https://rubygems.org/
|
|
12
|
+
specs:
|
|
13
|
+
bigdecimal (4.0.1)
|
|
14
|
+
ffi (1.17.3)
|
|
15
|
+
ffi (1.17.3-aarch64-linux-gnu)
|
|
16
|
+
ffi (1.17.3-aarch64-linux-musl)
|
|
17
|
+
ffi (1.17.3-arm-linux-gnu)
|
|
18
|
+
ffi (1.17.3-arm-linux-musl)
|
|
19
|
+
ffi (1.17.3-arm64-darwin)
|
|
20
|
+
ffi (1.17.3-x86-linux-gnu)
|
|
21
|
+
ffi (1.17.3-x86-linux-musl)
|
|
22
|
+
ffi (1.17.3-x86_64-darwin)
|
|
23
|
+
ffi (1.17.3-x86_64-linux-gnu)
|
|
24
|
+
ffi (1.17.3-x86_64-linux-musl)
|
|
25
|
+
google-protobuf (4.34.0)
|
|
26
|
+
bigdecimal
|
|
27
|
+
rake (~> 13.3)
|
|
28
|
+
google-protobuf (4.34.0-aarch64-linux-gnu)
|
|
29
|
+
bigdecimal
|
|
30
|
+
rake (~> 13.3)
|
|
31
|
+
google-protobuf (4.34.0-aarch64-linux-musl)
|
|
32
|
+
bigdecimal
|
|
33
|
+
rake (~> 13.3)
|
|
34
|
+
google-protobuf (4.34.0-arm64-darwin)
|
|
35
|
+
bigdecimal
|
|
36
|
+
rake (~> 13.3)
|
|
37
|
+
google-protobuf (4.34.0-x86-linux-gnu)
|
|
38
|
+
bigdecimal
|
|
39
|
+
rake (~> 13.3)
|
|
40
|
+
google-protobuf (4.34.0-x86-linux-musl)
|
|
41
|
+
bigdecimal
|
|
42
|
+
rake (~> 13.3)
|
|
43
|
+
google-protobuf (4.34.0-x86_64-darwin)
|
|
44
|
+
bigdecimal
|
|
45
|
+
rake (~> 13.3)
|
|
46
|
+
google-protobuf (4.34.0-x86_64-linux-gnu)
|
|
47
|
+
bigdecimal
|
|
48
|
+
rake (~> 13.3)
|
|
49
|
+
google-protobuf (4.34.0-x86_64-linux-musl)
|
|
50
|
+
bigdecimal
|
|
51
|
+
rake (~> 13.3)
|
|
52
|
+
googleapis-common-protos-types (1.22.0)
|
|
53
|
+
google-protobuf (~> 4.26)
|
|
54
|
+
grpc (1.78.1)
|
|
55
|
+
google-protobuf (>= 3.25, < 5.0)
|
|
56
|
+
googleapis-common-protos-types (~> 1.0)
|
|
57
|
+
grpc (1.78.1-aarch64-linux-gnu)
|
|
58
|
+
google-protobuf (>= 3.25, < 5.0)
|
|
59
|
+
googleapis-common-protos-types (~> 1.0)
|
|
60
|
+
grpc (1.78.1-aarch64-linux-musl)
|
|
61
|
+
google-protobuf (>= 3.25, < 5.0)
|
|
62
|
+
googleapis-common-protos-types (~> 1.0)
|
|
63
|
+
grpc (1.78.1-arm64-darwin)
|
|
64
|
+
google-protobuf (>= 3.25, < 5.0)
|
|
65
|
+
googleapis-common-protos-types (~> 1.0)
|
|
66
|
+
grpc (1.78.1-x86-linux-gnu)
|
|
67
|
+
google-protobuf (>= 3.25, < 5.0)
|
|
68
|
+
googleapis-common-protos-types (~> 1.0)
|
|
69
|
+
grpc (1.78.1-x86-linux-musl)
|
|
70
|
+
google-protobuf (>= 3.25, < 5.0)
|
|
71
|
+
googleapis-common-protos-types (~> 1.0)
|
|
72
|
+
grpc (1.78.1-x86_64-darwin)
|
|
73
|
+
google-protobuf (>= 3.25, < 5.0)
|
|
74
|
+
googleapis-common-protos-types (~> 1.0)
|
|
75
|
+
grpc (1.78.1-x86_64-linux-gnu)
|
|
76
|
+
google-protobuf (>= 3.25, < 5.0)
|
|
77
|
+
googleapis-common-protos-types (~> 1.0)
|
|
78
|
+
grpc (1.78.1-x86_64-linux-musl)
|
|
79
|
+
google-protobuf (>= 3.25, < 5.0)
|
|
80
|
+
googleapis-common-protos-types (~> 1.0)
|
|
81
|
+
lru_redux (1.1.0)
|
|
82
|
+
msgpack (1.8.0)
|
|
83
|
+
rake (13.3.1)
|
|
84
|
+
|
|
85
|
+
PLATFORMS
|
|
86
|
+
aarch64-linux-gnu
|
|
87
|
+
aarch64-linux-musl
|
|
88
|
+
arm-linux-gnu
|
|
89
|
+
arm-linux-musl
|
|
90
|
+
arm64-darwin
|
|
91
|
+
ruby
|
|
92
|
+
x86-linux-gnu
|
|
93
|
+
x86-linux-musl
|
|
94
|
+
x86_64-darwin
|
|
95
|
+
x86_64-linux-gnu
|
|
96
|
+
x86_64-linux-musl
|
|
97
|
+
|
|
98
|
+
DEPENDENCIES
|
|
99
|
+
crudjt!
|
|
100
|
+
|
|
101
|
+
BUNDLED WITH
|
|
102
|
+
2.6.7
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 v_akymov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="logos/crudjt_logo_white_on_dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="logos/crudjt_logo_dark_on_white.svg">
|
|
5
|
+
<img alt="Shows a dark logo" src="logos/crudjt_logo_dark.png">
|
|
6
|
+
</picture>
|
|
7
|
+
</br>
|
|
8
|
+
Ruby SDK for the fast, file-backed, scalable JSON token engine
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.patreon.com/crudjt">
|
|
13
|
+
<img src="logos/buy_me_a_coffee_orange.svg" alt="Buy Me a Coffee"/>
|
|
14
|
+
</a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
> ⚠️ Version 1.0.0-beta — production testing phase
|
|
18
|
+
> API is stable. Feedback is welcome before the final 1.0.0 release
|
|
19
|
+
|
|
20
|
+
Fast B-tree–backed token store for stateful user sessions
|
|
21
|
+
Provides authentication and authorization across multiple processes
|
|
22
|
+
Optimized for vertical scaling on a single server
|
|
23
|
+
|
|
24
|
+
# Installation
|
|
25
|
+
|
|
26
|
+
With Bundler:
|
|
27
|
+
```sh
|
|
28
|
+
bundle add crudjt
|
|
29
|
+
```
|
|
30
|
+
Or via RubyGems:
|
|
31
|
+
```sh
|
|
32
|
+
gem install crudjt
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## How to use
|
|
36
|
+
|
|
37
|
+
- One process starts the master
|
|
38
|
+
- All other processes connect to it
|
|
39
|
+
|
|
40
|
+
## Start CRUDJT master (once)
|
|
41
|
+
|
|
42
|
+
Start the CRUDJT master when your application boots
|
|
43
|
+
|
|
44
|
+
Only **one process** can do this for a **single token storage**
|
|
45
|
+
|
|
46
|
+
The master process manages sessions and coordination
|
|
47
|
+
All functions can also be used directly from it
|
|
48
|
+
|
|
49
|
+
### Generate a new secret key (terminal)
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
export CRUDJT_SECRET_KEY=$(openssl rand -base64 48)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Start master (ruby)
|
|
56
|
+
```ruby
|
|
57
|
+
require 'crudjt'
|
|
58
|
+
|
|
59
|
+
CRUDJT::Config.start_master(
|
|
60
|
+
secret_key: ENV.fetch('CRUDJT_SECRET_KEY'),
|
|
61
|
+
store_jt_path: 'path/to/local/storage', # optional
|
|
62
|
+
grpc_host: '127.0.0.1', # default
|
|
63
|
+
grpc_port: 50051 # default
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
*Important: Use the same `secret_key` across all sessions. If the key changes, previously stored tokens cannot be decrypted and will return `nil` or `false`*
|
|
68
|
+
|
|
69
|
+
## Start CRUDJT master in Docker
|
|
70
|
+
> `docker-compose.yml` will be published after 1.0.0-beta Docker image builds
|
|
71
|
+
|
|
72
|
+
## Connect to an existing CRUDJT master
|
|
73
|
+
|
|
74
|
+
Use this in all other processes
|
|
75
|
+
|
|
76
|
+
Typical examples:
|
|
77
|
+
- multiple local processes
|
|
78
|
+
- background jobs
|
|
79
|
+
- forked processes
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
require 'crudjt'
|
|
83
|
+
|
|
84
|
+
CRUDJT::Config.connect_to_master(
|
|
85
|
+
grpc_host: '127.0.0.1', # default
|
|
86
|
+
grpc_port: 50051 # default
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Process layout
|
|
91
|
+
|
|
92
|
+
App boot
|
|
93
|
+
├─ Process A → start_master
|
|
94
|
+
├─ Process B → connect_to_master
|
|
95
|
+
└─ Process C → connect_to_master
|
|
96
|
+
|
|
97
|
+
# C
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
data = { user_id: 42, role: 11 } # required
|
|
101
|
+
ttl = 3600 * 24 * 30 # optional: token lifetime (seconds)
|
|
102
|
+
|
|
103
|
+
# Optional: read limit
|
|
104
|
+
# Each read decrements the counter
|
|
105
|
+
# When it reaches zero — the token is deleted
|
|
106
|
+
silence_read = 10
|
|
107
|
+
|
|
108
|
+
token = CRUDJT.create(data, ttl: ttl, silence_read: silence_read)
|
|
109
|
+
# token == 'HBmKFXoXgJ46mCqer1WXyQ'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
# To disable token expiration or read limits, pass `nil`
|
|
114
|
+
CRUDJT.create({ user_id: 42, role: 11 }, ttl: nil, silence_read: nil)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
# R
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
result = CRUDJT.read('HBmKFXoXgJ46mCqer1WXyQ')
|
|
121
|
+
# result == {'metadata' => {'ttl' => 101001, 'silence_read' => 9}, 'data' => {'user_id' => 42, 'role' => 11}}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# When expired or not found token
|
|
126
|
+
result = CRUDJT.read('HBmKFXoXgJ46mCqer1WXyQ')
|
|
127
|
+
# result == nil
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
# U
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
data = { user_id: 42, role: 8 }
|
|
134
|
+
# `nil` disables limits
|
|
135
|
+
ttl = 600
|
|
136
|
+
silence_read = 100
|
|
137
|
+
|
|
138
|
+
result = CRUDJT.update('HBmKFXoXgJ46mCqer1WXyQ', data, ttl: ttl, silence_read: silence_read)
|
|
139
|
+
# result == true
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
# When expired or not found token
|
|
144
|
+
CRUDJT.update('HBmKFXoXgJ46mCqer1WXyQ', { user_id: 42, role: 8 })
|
|
145
|
+
# result == false
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
# D
|
|
149
|
+
```ruby
|
|
150
|
+
result = CRUDJT.delete('HBmKFXoXgJ46mCqer1WXyQ')
|
|
151
|
+
# result == true
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
# When expired or not found token
|
|
156
|
+
result = CRUDJT.delete('HBmKFXoXgJ46mCqer1WXyQ')
|
|
157
|
+
# result == false
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
# Performance
|
|
161
|
+
> Metrics will be published after 1.0.0-beta GitHub Actions builds
|
|
162
|
+
|
|
163
|
+
# Storage (File-backed)
|
|
164
|
+
|
|
165
|
+
## Disk footprint
|
|
166
|
+
> Metrics will be published after 1.0.0-beta GitHub Actions builds
|
|
167
|
+
|
|
168
|
+
## Path Lookup Order
|
|
169
|
+
Stored tokens are placed in the **file system** according to the following order
|
|
170
|
+
|
|
171
|
+
1. Explicitly set via `CRUDJT::Config.start_master(store_jt_path: 'custom/path/to/file_system_db')`
|
|
172
|
+
2. Default system location
|
|
173
|
+
- **Linux**: `/var/lib/store_jt`
|
|
174
|
+
- **macOS**: `/usr/local/var/store_jt`
|
|
175
|
+
- **Windows**: `C:\Program Files\store_jt`
|
|
176
|
+
3. Project root directory (fallback)
|
|
177
|
+
|
|
178
|
+
## Storage Characteristics
|
|
179
|
+
* CRUDJT **automatically removing expired tokens** after start and every 24 hours without blocking the main thread
|
|
180
|
+
* **Storage automatically fsyncs every 500ms**, meanwhile tokens are available from cache
|
|
181
|
+
|
|
182
|
+
# Multi-process Coordination
|
|
183
|
+
For multi-process scenarios, CRUDJT uses gRPC over an insecure local port for same-host communication only. It is not intended for inter-machine or internet-facing usage
|
|
184
|
+
|
|
185
|
+
# Limits
|
|
186
|
+
The library has the following limits and requirements
|
|
187
|
+
|
|
188
|
+
- **Ruby version:** tested with 2.7
|
|
189
|
+
- **Supported platforms:** Linux, macOS (x86_64 / arm64). Windows (experimental, x86_64 / arm64)
|
|
190
|
+
- **Maximum json size per token:** 256 bytes
|
|
191
|
+
- **`secret_key` format:** must be Base64
|
|
192
|
+
- **`secret_key` size:** must be 32, 48, or 64 bytes
|
|
193
|
+
|
|
194
|
+
# Contact & Support
|
|
195
|
+
<p align="center">
|
|
196
|
+
<picture>
|
|
197
|
+
<source media="(prefers-color-scheme: dark)" srcset="logos/crudjt_favicon_160x160_white_on_dark.svg" width=160 height=160>
|
|
198
|
+
<source media="(prefers-color-scheme: light)" srcset="logos/crudjt_favicon_160x160_dark_on_white.svg" width=160 height=160>
|
|
199
|
+
<img alt="Shows a dark favicon in light color mode and a white one in dark color mode" src="logos/crudjt_favicon_160x160_white.png" width=160 height=160>
|
|
200
|
+
</picture>
|
|
201
|
+
</p>
|
|
202
|
+
|
|
203
|
+
- **Custom integrations / new features / collaboration**: support@crudjt.com
|
|
204
|
+
- **Library support & bug reports:** [open an issue](https://github.com/crudjt/crudjt-ruby/issues)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# Lincense
|
|
208
|
+
CRUDJT is released under the [MIT License](LICENSE.txt)
|
|
209
|
+
|
|
210
|
+
<p align="center">
|
|
211
|
+
💘 Shoot your g . ? Love me out via <a href="https://www.patreon.com/crudjt">Patreon Sponsors</a>!
|
|
212
|
+
</p>
|
data/Rakefile
ADDED
data/autotest.rb
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
require 'benchmark'
|
|
2
|
+
require 'crudjt'
|
|
3
|
+
|
|
4
|
+
if ENV['CRUDJT_AUTOTEST_ALLOWED'] != 'true'
|
|
5
|
+
return STDOUT.puts "Denied run autotest for this environment. Set ENV['CRUDJT_AUTOTEST_ALLOWED'] = 'true'"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
p "OS: #{RbConfig::CONFIG['host_os']}"
|
|
9
|
+
p "CPU: #{RbConfig::CONFIG['host_cpu']}"
|
|
10
|
+
|
|
11
|
+
p 'Checking secret key validations...'
|
|
12
|
+
# when started without secret key
|
|
13
|
+
begin
|
|
14
|
+
CRUDJT::Config.start_master
|
|
15
|
+
rescue RuntimeError => error
|
|
16
|
+
p error.message == CRUDJT::Validation.error_message(CRUDJT::Validation::ERROR_SECRET_KEY_NOT_SET)
|
|
17
|
+
else
|
|
18
|
+
p false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# when started with fake base64 secret key
|
|
22
|
+
begin
|
|
23
|
+
CRUDJT::Config.start_master(secret_key: 'bla-bla-bla')
|
|
24
|
+
rescue ArgumentError => error
|
|
25
|
+
p error.message == "'secret_key' must be a valid Base64 string"
|
|
26
|
+
else
|
|
27
|
+
p false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# when started with wrong secret key lenght
|
|
31
|
+
begin
|
|
32
|
+
key_16_bytes = '2v+XIslTkPTfjva0xeCLHQ=='
|
|
33
|
+
CRUDJT::Config.start_master(secret_key: key_16_bytes)
|
|
34
|
+
rescue ArgumentError => error
|
|
35
|
+
p error.message == "'secret_key' must be exactly 32, 48, or 64 bytes. Got #{Base64.strict_decode64(key_16_bytes).bytesize} bytes"
|
|
36
|
+
else
|
|
37
|
+
p false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
p 'Checking base validations...'
|
|
41
|
+
|
|
42
|
+
# when not started store_jt
|
|
43
|
+
begin
|
|
44
|
+
CRUDJT.original_create({ some_key: 'some value' })
|
|
45
|
+
rescue RuntimeError => error
|
|
46
|
+
p error.message == CRUDJT::Validation.error_message(CRUDJT::Validation::ERROR_NOT_STARTED)
|
|
47
|
+
else
|
|
48
|
+
p false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# when not started store_jt
|
|
52
|
+
begin
|
|
53
|
+
CRUDJT.original_create({ some_key: 'some value' })
|
|
54
|
+
rescue RuntimeError => error
|
|
55
|
+
p error.message == CRUDJT::Validation.error_message(CRUDJT::Validation::ERROR_NOT_STARTED)
|
|
56
|
+
else
|
|
57
|
+
p false unless RbConfig::CONFIG['host_os'].include?('w32')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
CRUDJT::Config.start_master(
|
|
61
|
+
secret_key: 'Cm7B68NWsMNNYjzMDREacmpe5sI1o0g40ZC9w1yQW3WOes7Gm59UsittLOHR2dciYiwmaYq98l3tG8h9yXVCxg=='
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
p "Master: #{CRUDJT::Config.master?}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# without metadata
|
|
68
|
+
p 'Checking without metadata...'
|
|
69
|
+
data = { user_id: 42, role: 11 }
|
|
70
|
+
expected_data = { data: data.transform_keys(&:to_s) }.transform_keys(&:to_s)
|
|
71
|
+
|
|
72
|
+
updated_data = { user_id: 42, role: 8 }
|
|
73
|
+
expected_updated_data = { data: updated_data.transform_keys(&:to_s) }.transform_keys(&:to_s)
|
|
74
|
+
|
|
75
|
+
token = CRUDJT.create(data)
|
|
76
|
+
|
|
77
|
+
p CRUDJT.read(token) == expected_data
|
|
78
|
+
p CRUDJT.update(token, updated_data) == true
|
|
79
|
+
p CRUDJT.read(token) == expected_updated_data
|
|
80
|
+
p CRUDJT.delete(token) == true
|
|
81
|
+
p CRUDJT.read(token) == nil
|
|
82
|
+
|
|
83
|
+
# with ttl
|
|
84
|
+
p 'Checking ttl...'
|
|
85
|
+
|
|
86
|
+
data = { user_id: 42, role: 11 }
|
|
87
|
+
|
|
88
|
+
ttl = 5
|
|
89
|
+
token_with_ttl = CRUDJT.create(data, ttl: ttl)
|
|
90
|
+
|
|
91
|
+
expected_ttl = ttl
|
|
92
|
+
ttl.times do |i|
|
|
93
|
+
p CRUDJT.read(token_with_ttl) == JSON.parse({ metadata: { ttl: expected_ttl }, data: data }.to_json)
|
|
94
|
+
expected_ttl -= 1
|
|
95
|
+
|
|
96
|
+
sleep 1
|
|
97
|
+
end
|
|
98
|
+
p CRUDJT.read(token_with_ttl) == nil
|
|
99
|
+
|
|
100
|
+
# when expired ttl
|
|
101
|
+
p 'when expired ttl'
|
|
102
|
+
data = { user_id: 42, role: 11 }
|
|
103
|
+
ttl = 1
|
|
104
|
+
token = CRUDJT.create(data, ttl: ttl)
|
|
105
|
+
sleep ttl
|
|
106
|
+
p CRUDJT.read(token) == nil
|
|
107
|
+
p CRUDJT.update(token, data) == false
|
|
108
|
+
p CRUDJT.delete(token) == false
|
|
109
|
+
|
|
110
|
+
p CRUDJT.update(token, data) == false
|
|
111
|
+
p CRUDJT.read(token) == nil
|
|
112
|
+
|
|
113
|
+
# with silence read
|
|
114
|
+
p "Checking silence read..."
|
|
115
|
+
|
|
116
|
+
data = { user_id: 42, role: 11 }
|
|
117
|
+
silence_read = 6
|
|
118
|
+
token_with_silence_read = CRUDJT.create(data, silence_read: silence_read)
|
|
119
|
+
|
|
120
|
+
expected_silence_read = silence_read - 1
|
|
121
|
+
silence_read.times do
|
|
122
|
+
p CRUDJT.read(token_with_silence_read) == JSON.parse({ metadata: { silence_read: expected_silence_read }, data: data }.to_json)
|
|
123
|
+
expected_silence_read -= 1
|
|
124
|
+
end
|
|
125
|
+
p CRUDJT.read(token_with_silence_read) == nil
|
|
126
|
+
|
|
127
|
+
# with ttl and silence read
|
|
128
|
+
p "Checking ttl and silence read..."
|
|
129
|
+
|
|
130
|
+
data = { user_id: 42, role: 11 }
|
|
131
|
+
ttl = 5
|
|
132
|
+
silence_read = ttl
|
|
133
|
+
token_with_ttl_and_silence_read = CRUDJT.create(data, ttl: ttl, silence_read: silence_read)
|
|
134
|
+
|
|
135
|
+
expected_ttl = ttl
|
|
136
|
+
expected_silence_read = silence_read - 1
|
|
137
|
+
silence_read.times do
|
|
138
|
+
p CRUDJT.read(token_with_ttl_and_silence_read) == JSON.parse({ metadata: { ttl: expected_ttl, silence_read: expected_silence_read }, data: data }.to_json)
|
|
139
|
+
expected_ttl -= 1
|
|
140
|
+
expected_silence_read -= 1
|
|
141
|
+
|
|
142
|
+
sleep 1
|
|
143
|
+
end
|
|
144
|
+
p CRUDJT.read(token_with_ttl_and_silence_read) == nil
|
|
145
|
+
|
|
146
|
+
# with scale load
|
|
147
|
+
|
|
148
|
+
REQUESTS = 40_000
|
|
149
|
+
|
|
150
|
+
data = {user_id: 414243, role: 11, devices: {ios_expired_at: Time.now.to_s, android_expired_at: Time.now.to_s, external_api_integration_expired_at: Time.now.to_s}, a: "a" * 100 }
|
|
151
|
+
while MessagePack.pack(data).bytesize > CRUDJT::Validation::MAX_HASH_SIZE
|
|
152
|
+
data[:a].chop!
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
updated_data = { user_id: 42, role: 11 }
|
|
156
|
+
|
|
157
|
+
p "Hash bytesize: #{MessagePack.pack(data).bytesize}"
|
|
158
|
+
values = []
|
|
159
|
+
10.times do
|
|
160
|
+
tokens = []
|
|
161
|
+
|
|
162
|
+
p 'Checking scale load...'
|
|
163
|
+
|
|
164
|
+
# when create
|
|
165
|
+
p 'when creates 40k tokens'
|
|
166
|
+
puts Benchmark.measure { REQUESTS.times { |i| tokens << CRUDJT.create(data) } }
|
|
167
|
+
|
|
168
|
+
# when read
|
|
169
|
+
p 'when reads 40k tokens'
|
|
170
|
+
index = rand(0..REQUESTS)
|
|
171
|
+
puts Benchmark.measure { REQUESTS.times { |i| CRUDJT.read(tokens[i]) } }
|
|
172
|
+
|
|
173
|
+
# when updates
|
|
174
|
+
p 'when updates 40k tokens'
|
|
175
|
+
puts Benchmark.measure { REQUESTS.times { |i| CRUDJT.update(tokens[i], updated_data) } }
|
|
176
|
+
#
|
|
177
|
+
# when delete
|
|
178
|
+
p 'when deletes 40k tokens'
|
|
179
|
+
puts Benchmark.measure { REQUESTS.times { |i| CRUDJT.delete(tokens[i]) } }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# when cache after read from file system
|
|
183
|
+
p 'when caches after read from file system'
|
|
184
|
+
|
|
185
|
+
LIMIT_ON_READY_FOR_CACHE = 2
|
|
186
|
+
|
|
187
|
+
previus_tokens = []
|
|
188
|
+
|
|
189
|
+
REQUESTS.times { previus_tokens << CRUDJT.create(data) }
|
|
190
|
+
REQUESTS.times { CRUDJT.create(data) }
|
|
191
|
+
|
|
192
|
+
LIMIT_ON_READY_FOR_CACHE.times { puts Benchmark.measure { REQUESTS.times { |i| CRUDJT.read(previus_tokens[i]) } } }
|
data/bin/console
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "crudjt"
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
+
# require "pry"
|
|
11
|
+
# Pry.start
|
|
12
|
+
|
|
13
|
+
require "irb"
|
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/crudjt.gemspec
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require_relative 'lib/crudjt/version'
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "crudjt"
|
|
5
|
+
spec.version = CRUDJT::VERSION
|
|
6
|
+
spec.authors = ["Vlad Akymov (v_akymov)"]
|
|
7
|
+
spec.email = ["support@crudjt.com"]
|
|
8
|
+
|
|
9
|
+
spec.summary = %q{Fast B-tree–backed token store for stateful sessions}
|
|
10
|
+
spec.description = <<~DESC
|
|
11
|
+
Fast B-tree–backed token store for stateful user sessions
|
|
12
|
+
Provides authentication and authorization across multiple processes
|
|
13
|
+
Optimized for vertical scaling on a single server
|
|
14
|
+
DESC
|
|
15
|
+
spec.homepage = "https://github.com/crudjt"
|
|
16
|
+
spec.license = "MIT"
|
|
17
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
|
18
|
+
|
|
19
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
20
|
+
|
|
21
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
22
|
+
spec.metadata["source_code_uri"] = "https://github.com/crudjt/crudjt-ruby"
|
|
23
|
+
spec.metadata["documentation_uri"] = "https://github.com/crudjt/crudjt-ruby#readme"
|
|
24
|
+
spec.metadata["changelog_uri"] = "https://github.com/crudjt/crudjt-ruby/blob/master/CHANGELOG.md"
|
|
25
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/crudjt/crudjt-ruby/issues"
|
|
26
|
+
spec.metadata["funding_uri"] = "https://patreon.com/crudjt"
|
|
27
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
28
|
+
spec.metadata["keywords"] = "auth, authentication, token, sessions, crud"
|
|
29
|
+
|
|
30
|
+
spec.add_dependency "ffi", "~> 1.17"
|
|
31
|
+
spec.add_dependency "msgpack", "~> 1.8"
|
|
32
|
+
spec.add_dependency "lru_redux", "~> 1.1"
|
|
33
|
+
spec.add_dependency "grpc", "~> 1.78.0"
|
|
34
|
+
|
|
35
|
+
# Specify which files should be added to the gem when it is released.
|
|
36
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
37
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
|
38
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
spec.bindir = "exe"
|
|
42
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
43
|
+
spec.require_paths = ["lib"]
|
|
44
|
+
end
|