memori-client 0.1.2 → 0.1.6
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/lib/memori_client/backend/v1/asset.rb +1 -1
- data/lib/memori_client/backend/v2/action_log.rb +1 -1
- data/lib/memori_client/backend/v2/analysis.rb +1 -1
- data/lib/memori_client/backend/v2/asset.rb +1 -1
- data/lib/memori_client/backend/v2/badge.rb +1 -1
- data/lib/memori_client/backend/v2/completion_config.rb +1 -1
- data/lib/memori_client/backend/v2/consumption_log.rb +1 -1
- data/lib/memori_client/backend/v2/import_export.rb +1 -1
- data/lib/memori_client/backend/v2/integration.rb +1 -1
- data/lib/memori_client/backend/v2/invitation.rb +1 -1
- data/lib/memori_client/backend/v2/memori.rb +1 -1
- data/lib/memori_client/backend/v2/memori_list.rb +1 -1
- data/lib/memori_client/backend/v2/notification.rb +1 -1
- data/lib/memori_client/backend/v2/process.rb +1 -1
- data/lib/memori_client/backend/v2/tenant.rb +1 -1
- data/lib/memori_client/backend/v2/user.rb +1 -1
- data/lib/memori_client/configuration.rb +5 -0
- data/lib/memori_client/engine/hmac_helper.rb +186 -0
- data/lib/memori_client/engine/resource.rb +5 -31
- data/lib/memori_client/engine/v2/chat_log.rb +1 -1
- data/lib/memori_client/engine/v2/context_var.rb +1 -1
- data/lib/memori_client/engine/v2/correlation_pair.rb +1 -1
- data/lib/memori_client/engine/v2/custom_dictionary.rb +1 -1
- data/lib/memori_client/engine/v2/dialog.rb +1 -1
- data/lib/memori_client/engine/v2/event_log.rb +1 -1
- data/lib/memori_client/engine/v2/expert_reference.rb +1 -1
- data/lib/memori_client/engine/v2/function.rb +1 -1
- data/lib/memori_client/engine/v2/intent.rb +1 -1
- data/lib/memori_client/engine/v2/localization_key.rb +1 -1
- data/lib/memori_client/engine/v2/medium.rb +1 -1
- data/lib/memori_client/engine/v2/memory.rb +1 -1
- data/lib/memori_client/engine/v2/nlp.rb +1 -1
- data/lib/memori_client/engine/v2/person.rb +1 -1
- data/lib/memori_client/engine/v2/private/memori.rb +17 -0
- data/lib/memori_client/engine/v2/private/memori_block.rb +24 -0
- data/lib/memori_client/engine/v2/search.rb +1 -1
- data/lib/memori_client/engine/v2/session.rb +1 -1
- data/lib/memori_client/engine/v2/stat.rb +1 -1
- data/lib/memori_client/engine/v2/topic.rb +1 -1
- data/lib/memori_client/engine/v2/unanswered_question.rb +1 -1
- data/lib/memori_client/engine/v2/user.rb +1 -1
- data/lib/memori_client/engine/v2/web_hook.rb +1 -1
- data/lib/memori_client/http_client.rb +8 -1
- data/lib/memori_client/resource.rb +3 -2
- data/lib/memori_client.rb +6 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c10fc0d10bce1ed59b4958dbf180ea960feff117101b2c9376a2bb584db64fe
|
4
|
+
data.tar.gz: 8b6274442472d9c9a175df35e07668066a857de6a022761fbca766c15fae4d74
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5e5b9e4607847f63fb0de2919799773187d50740c1554cb3466c797ffd2d82cfc8547b33ee6c6967f09aa4369671560cb13eeca14be0a3ca1989553ca43fe9d
|
7
|
+
data.tar.gz: 7115e5babdddb0b6e7448a60de84fc92875d5051011ad975e77ad40a45e6db4110362bcea2419aa0de7aa69fb9d601f89206ecbddb53ef92bbfadef398f97c1d
|
@@ -5,6 +5,11 @@ module MemoriClient
|
|
5
5
|
attr_accessor :backend_api_password
|
6
6
|
attr_accessor :backend_api_tenant
|
7
7
|
attr_accessor :engine_api_root
|
8
|
+
attr_accessor :engine_private_api_root
|
9
|
+
|
10
|
+
# Used to authorize private API calls to the engine
|
11
|
+
attr_accessor :engine_app_id
|
12
|
+
attr_accessor :engine_api_key
|
8
13
|
|
9
14
|
# Initialize every configuration with a default.
|
10
15
|
# Users of the gem will override these with their
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
class MemoriClient::Engine::HMACHelper
|
6
|
+
IPAD = 0x36
|
7
|
+
OPAD = 0x5c
|
8
|
+
|
9
|
+
def initialize(app_id, key, max_delta_secs = 30)
|
10
|
+
# Check arguments
|
11
|
+
raise ArgumentError, "HMAC: invalid appID" if app_id.nil?
|
12
|
+
raise ArgumentError, "HMAC: invalid key" if key.nil? || key.to_s == "00000000-0000-0000-0000-000000000000"
|
13
|
+
|
14
|
+
# Initialization
|
15
|
+
@app_id = app_id
|
16
|
+
@max_delta_secs = max_delta_secs
|
17
|
+
|
18
|
+
@message_cache = {}
|
19
|
+
@cache_mutex = Mutex.new
|
20
|
+
|
21
|
+
@@next_nonce ||= 0
|
22
|
+
@nonce_mutex = Mutex.new
|
23
|
+
|
24
|
+
# Build inner and outer keys
|
25
|
+
key_bytes = guid_to_bytes(key)
|
26
|
+
|
27
|
+
inner_key_bytes = key_bytes.map { |byte| byte ^ IPAD }
|
28
|
+
@inner_key = inner_key_bytes.map { |byte| byte.to_s(16).rjust(2, '0') }.join
|
29
|
+
|
30
|
+
outer_key_bytes = key_bytes.map { |byte| byte ^ OPAD }
|
31
|
+
@outer_key = outer_key_bytes.map { |byte| byte.to_s(16).rjust(2, '0') }.join
|
32
|
+
|
33
|
+
# Start a thread for cache expiration
|
34
|
+
Thread.new do
|
35
|
+
loop do
|
36
|
+
clean_cache
|
37
|
+
sleep 1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Not meant to be invoked directly, but could be used for testing
|
43
|
+
# @return [Hash] the message and the signed HMAC
|
44
|
+
def build_hmac(nonce:, epoch:)
|
45
|
+
message = "#{@app_id}:#{nonce}:#{epoch}"
|
46
|
+
|
47
|
+
# Sign the message
|
48
|
+
signature = sign(message, @inner_key, @outer_key)
|
49
|
+
[message, signature]
|
50
|
+
end
|
51
|
+
|
52
|
+
def next_hmac
|
53
|
+
nonce = @nonce_mutex.synchronize { @@next_nonce += 1 }
|
54
|
+
epoch = Time.now.utc.to_i # Use UTC time like C#
|
55
|
+
message, signature = build_hmac(nonce: nonce, epoch: epoch)
|
56
|
+
|
57
|
+
"#{message}:#{signature}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def verify_hmac(hmac)
|
61
|
+
# Check arguments
|
62
|
+
raise ArgumentError, "HMAC: invalid message" if hmac.nil? || hmac.empty?
|
63
|
+
|
64
|
+
# String split HMAC message
|
65
|
+
parts = hmac.split(':')
|
66
|
+
raise ArgumentError, "HMAC: invalid message format" if parts.length != 4
|
67
|
+
|
68
|
+
# Check the app ID
|
69
|
+
begin
|
70
|
+
app_id = Integer(parts[0])
|
71
|
+
rescue
|
72
|
+
raise ArgumentError, "HMAC: invalid app ID"
|
73
|
+
end
|
74
|
+
|
75
|
+
raise ArgumentError, "HMAC: unrecognized app ID" if app_id != @app_id
|
76
|
+
|
77
|
+
# Check the nonce
|
78
|
+
begin
|
79
|
+
nonce = Integer(parts[1])
|
80
|
+
rescue
|
81
|
+
raise ArgumentError, "HMAC: invalid nonce"
|
82
|
+
end
|
83
|
+
|
84
|
+
raise ArgumentError, "HMAC: invalid nonce" if nonce < 1
|
85
|
+
|
86
|
+
# Check the timestamp
|
87
|
+
begin
|
88
|
+
timestamp = Integer(parts[2])
|
89
|
+
rescue
|
90
|
+
raise ArgumentError, "HMAC: invalid timestamp"
|
91
|
+
end
|
92
|
+
|
93
|
+
epoch = Time.now.utc.to_i # Use UTC time like C#
|
94
|
+
time_diff = epoch - timestamp
|
95
|
+
raise ArgumentError, "HMAC: timestamp out of range" if time_diff <= -@max_delta_secs || time_diff >= @max_delta_secs
|
96
|
+
|
97
|
+
# Check the signature
|
98
|
+
message = "#{app_id}:#{nonce}:#{timestamp}"
|
99
|
+
expected_signature = sign(message, @inner_key, @outer_key)
|
100
|
+
actual_signature = parts[3]
|
101
|
+
|
102
|
+
# Use a constant-time comparison to prevent timing attacks
|
103
|
+
signature_valid = secure_compare(expected_signature, actual_signature)
|
104
|
+
raise ArgumentError, "HMAC: wrong signature" unless signature_valid
|
105
|
+
|
106
|
+
# Check if the message has already been seen
|
107
|
+
is_new = false
|
108
|
+
|
109
|
+
@cache_mutex.synchronize do
|
110
|
+
unless @message_cache.key?(message)
|
111
|
+
@message_cache[message] = Time.now.to_i + 30
|
112
|
+
is_new = true
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
raise ArgumentError, "HMAC: message already seen" unless is_new
|
117
|
+
|
118
|
+
true
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def sign(message, inner_key, outer_key)
|
124
|
+
# Compute the inner MD5 hash exactly like C#
|
125
|
+
inner_hash_bytes = Digest::MD5.digest(inner_key + message)
|
126
|
+
inner_hash_str = inner_hash_bytes.unpack1('H*')
|
127
|
+
|
128
|
+
# Compute the outer MD5 hash
|
129
|
+
outer_hash_bytes = Digest::MD5.digest(outer_key + inner_hash_str)
|
130
|
+
outer_hash_str = outer_hash_bytes.unpack1('H*')
|
131
|
+
|
132
|
+
return outer_hash_str
|
133
|
+
end
|
134
|
+
|
135
|
+
def guid_to_bytes(guid)
|
136
|
+
# Convert GUID to string if it's already an object
|
137
|
+
guid_str = guid.to_s
|
138
|
+
|
139
|
+
# Remove any curly braces and hyphens, standardize format
|
140
|
+
guid_str = guid_str.gsub(/[{}-]/, '')
|
141
|
+
|
142
|
+
# Ensure we have a valid 32-character GUID
|
143
|
+
raise ArgumentError, "Invalid GUID format" unless guid_str.length == 32
|
144
|
+
|
145
|
+
# Parse the GUID using the same byte ordering as .NET's Guid.ToByteArray()
|
146
|
+
# This matches the specific layout: Int32, Int16, Int16, byte[8]
|
147
|
+
|
148
|
+
# First 4 bytes (8 hex chars) - need to reverse byte order (little-endian)
|
149
|
+
int32_bytes = [guid_str[0, 8]].pack('H*').bytes.reverse
|
150
|
+
|
151
|
+
# Next 2 bytes (4 hex chars) - need to reverse byte order (little-endian)
|
152
|
+
int16_1_bytes = [guid_str[8, 4]].pack('H*').bytes.reverse
|
153
|
+
|
154
|
+
# Next 2 bytes (4 hex chars) - need to reverse byte order (little-endian)
|
155
|
+
int16_2_bytes = [guid_str[12, 4]].pack('H*').bytes.reverse
|
156
|
+
|
157
|
+
# Remaining 8 bytes (16 hex chars) - keep original byte order
|
158
|
+
remaining_bytes = [guid_str[16, 16]].pack('H*').bytes
|
159
|
+
|
160
|
+
# Combine all byte arrays in the correct order
|
161
|
+
return int32_bytes + int16_1_bytes + int16_2_bytes + remaining_bytes
|
162
|
+
end
|
163
|
+
|
164
|
+
def secure_compare(a, b)
|
165
|
+
return false if a.bytesize != b.bytesize
|
166
|
+
|
167
|
+
# Constant-time comparison
|
168
|
+
result = 0
|
169
|
+
a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
|
170
|
+
result == 0
|
171
|
+
end
|
172
|
+
|
173
|
+
def clean_cache
|
174
|
+
now = Time.now.to_i
|
175
|
+
@cache_mutex.synchronize do
|
176
|
+
# Remove expired entries
|
177
|
+
@message_cache.delete_if { |_, expiry| expiry < now }
|
178
|
+
|
179
|
+
# Enforce size limit
|
180
|
+
if @message_cache.size > 100000
|
181
|
+
sorted = @message_cache.sort_by { |_, expiry| expiry }
|
182
|
+
@message_cache = Hash[sorted.first(100000)]
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -1,39 +1,13 @@
|
|
1
1
|
class MemoriClient::Engine::Resource < MemoriClient::Resource
|
2
|
-
# def self.exec_http_request(method, path, args)
|
3
|
-
# stop = false
|
4
|
-
# processed_tokens = []
|
5
|
-
# path.split('/').each do |token|
|
6
|
-
# break if stop == true
|
7
|
-
# if token =~ /^{.*}$/
|
8
|
-
# param_name = token.match(/^{(.*)}$/).captures.first
|
9
|
-
# if args[param_name.to_sym].blank?
|
10
|
-
# stop = true
|
11
|
-
# else
|
12
|
-
# processed_tokens << args[param_name.to_sym]
|
13
|
-
# end
|
14
|
-
# else
|
15
|
-
# processed_tokens << token
|
16
|
-
# end
|
17
|
-
# end
|
18
|
-
#
|
19
|
-
# url = processed_tokens.join('/')
|
20
|
-
# url = [MemoriClient.configuration.engine_api_root, url].join('')
|
21
|
-
# http = MemoriClient::HttpClient.new
|
22
|
-
#
|
23
|
-
# case method
|
24
|
-
# when 'get'
|
25
|
-
# status, body = http.get(url)
|
26
|
-
# else
|
27
|
-
# status, body = http.send(method, url, payload: args[:payload])
|
28
|
-
# end
|
29
|
-
#
|
30
|
-
# [status, body]
|
31
|
-
# end
|
32
|
-
|
33
2
|
def self.build_url(url)
|
34
3
|
[
|
35
4
|
MemoriClient.configuration.engine_api_root,
|
36
5
|
url
|
37
6
|
].join('')
|
38
7
|
end
|
8
|
+
|
9
|
+
def self.hmac_authorization_header
|
10
|
+
helper = MemoriClient::Engine::HMACHelper.new(MemoriClient.configuration.engine_app_id, MemoriClient.configuration.engine_api_key)
|
11
|
+
["HMAC", helper.next_hmac].join(' ')
|
12
|
+
end
|
39
13
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# This code is not generated by the swagger parser.
|
2
|
+
# Implements memori blocking and unblocking functionality
|
3
|
+
class MemoriClient::Engine::V2::Private::Memori < MemoriClient::Engine::Resource
|
4
|
+
def self.get_memori(strMemoriID:)
|
5
|
+
args = build_arguments(binding)
|
6
|
+
headers = { 'Authorization' => hmac_authorization_header }
|
7
|
+
args[:headers] = headers
|
8
|
+
exec_http_request('get', '/memori/v2/Memori/{strMemoriID}', **args)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.build_url(url)
|
12
|
+
[
|
13
|
+
MemoriClient.configuration.engine_private_api_root,
|
14
|
+
url
|
15
|
+
].join('')
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# This code is not generated by the swagger parser.
|
2
|
+
# Implements memori blocking and unblocking functionality
|
3
|
+
class MemoriClient::Engine::V2::Private::MemoriBlock < MemoriClient::Engine::Resource
|
4
|
+
def self.block_memori(strMemoriID:, strUntilDateTime:)
|
5
|
+
args = build_arguments(binding)
|
6
|
+
headers = { 'Authorization' => hmac_authorization_header }
|
7
|
+
args[:headers] = headers
|
8
|
+
exec_http_request('post', '/memori/v2/MemoriBlock/{strMemoriID}/{strUntilDateTime}', **args)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.unblock_memori(strMemoriID:)
|
12
|
+
args = build_arguments(binding)
|
13
|
+
headers = { 'Authorization' => hmac_authorization_header }
|
14
|
+
args[:headers] = headers
|
15
|
+
exec_http_request('delete', '/memori/v2/MemoriBlock/{strMemoriID}', **args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.build_url(url)
|
19
|
+
[
|
20
|
+
MemoriClient.configuration.engine_private_api_root,
|
21
|
+
url
|
22
|
+
].join('')
|
23
|
+
end
|
24
|
+
end
|
@@ -72,10 +72,17 @@ class MemoriClient::HttpClient
|
|
72
72
|
|
73
73
|
def handle_response(response, request)
|
74
74
|
body = response.read_body
|
75
|
+
|
75
76
|
if request['Content-Type'] == 'application/json'
|
77
|
+
if body.nil? || body == ''
|
78
|
+
response_body = {}
|
79
|
+
else
|
80
|
+
response_body = JSON.parse(body)
|
81
|
+
end
|
82
|
+
|
76
83
|
[
|
77
84
|
response.code,
|
78
|
-
(body.nil? ? {} :
|
85
|
+
(body.nil? ? {} : response_body)
|
79
86
|
]
|
80
87
|
else
|
81
88
|
[response.code, body]
|
@@ -7,6 +7,7 @@ class MemoriClient::Resource
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def self.exec_http_request(method, path, args)
|
10
|
+
headers = args[:headers] || {}
|
10
11
|
stop = false
|
11
12
|
processed_tokens = []
|
12
13
|
path.split('/').each do |token|
|
@@ -29,9 +30,9 @@ class MemoriClient::Resource
|
|
29
30
|
|
30
31
|
case method
|
31
32
|
when 'get'
|
32
|
-
status, body = http.get(url)
|
33
|
+
status, body = http.get(url, headers: headers)
|
33
34
|
else
|
34
|
-
status, body = http.send(method, url, payload: args[:payload])
|
35
|
+
status, body = http.send(method, url, payload: args[:payload], headers: headers)
|
35
36
|
end
|
36
37
|
|
37
38
|
[status, body]
|
data/lib/memori_client.rb
CHANGED
@@ -15,7 +15,9 @@ module MemoriClient
|
|
15
15
|
end
|
16
16
|
|
17
17
|
module V2
|
18
|
+
module Private
|
18
19
|
|
20
|
+
end
|
19
21
|
end
|
20
22
|
end
|
21
23
|
|
@@ -47,6 +49,10 @@ require "memori_client/http_client"
|
|
47
49
|
require "memori_client/resource"
|
48
50
|
require "memori_client/backend/resource"
|
49
51
|
require "memori_client/backend/resources"
|
52
|
+
require "memori_client/engine/hmac_helper"
|
50
53
|
require "memori_client/engine/resource"
|
51
54
|
require "memori_client/engine/resources"
|
52
55
|
|
56
|
+
require "memori_client/engine/v2/private/memori"
|
57
|
+
require "memori_client/engine/v2/private/memori_block"
|
58
|
+
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: memori-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefano Lampis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-04-30 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Memori Client to interact with Memori backend and engine API
|
14
14
|
email: me@stefanolampis.com
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- lib/memori_client/backend/v2/tenant.rb
|
37
37
|
- lib/memori_client/backend/v2/user.rb
|
38
38
|
- lib/memori_client/configuration.rb
|
39
|
+
- lib/memori_client/engine/hmac_helper.rb
|
39
40
|
- lib/memori_client/engine/resource.rb
|
40
41
|
- lib/memori_client/engine/resources.rb
|
41
42
|
- lib/memori_client/engine/v2/chat_log.rb
|
@@ -54,6 +55,8 @@ files:
|
|
54
55
|
- lib/memori_client/engine/v2/memory.rb
|
55
56
|
- lib/memori_client/engine/v2/nlp.rb
|
56
57
|
- lib/memori_client/engine/v2/person.rb
|
58
|
+
- lib/memori_client/engine/v2/private/memori.rb
|
59
|
+
- lib/memori_client/engine/v2/private/memori_block.rb
|
57
60
|
- lib/memori_client/engine/v2/prompted_question.rb
|
58
61
|
- lib/memori_client/engine/v2/search.rb
|
59
62
|
- lib/memori_client/engine/v2/session.rb
|