carddb 0.4.1 → 0.4.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.
- checksums.yaml +4 -4
- data/.rspec_status +135 -131
- data/README.md +15 -21
- data/lib/carddb/batch.rb +40 -6
- data/lib/carddb/collection.rb +22 -109
- data/lib/carddb/deck_tokens.rb +76 -61
- data/lib/carddb/query_builder.rb +44 -189
- data/lib/carddb/resources/decks.rb +94 -115
- data/lib/carddb/version.rb +1 -1
- metadata +2 -2
data/lib/carddb/collection.rb
CHANGED
|
@@ -1067,8 +1067,14 @@ module CardDB
|
|
|
1067
1067
|
def job_count = data['jobCount']
|
|
1068
1068
|
def completed_jobs = data['completedJobs']
|
|
1069
1069
|
def failed_jobs = data['failedJobs']
|
|
1070
|
+
def no_match_count = data['noMatchCount']
|
|
1070
1071
|
def average_confidence = data['averageConfidence']
|
|
1071
1072
|
def average_shortlist_size = data['averageShortlistSize']
|
|
1073
|
+
def vector_query_count = data['vectorQueryCount']
|
|
1074
|
+
def vector_fallback_count = data['vectorFallbackCount']
|
|
1075
|
+
def average_vector_candidates = data['averageVectorCandidates']
|
|
1076
|
+
def average_vector_query_ms = data['averageVectorQueryMs']
|
|
1077
|
+
def average_embedding_ms = data['averageEmbeddingMs']
|
|
1072
1078
|
def feedback_count = data['feedbackCount']
|
|
1073
1079
|
def correct_feedback = data['correctFeedback']
|
|
1074
1080
|
def corrected_feedback = data['correctedFeedback']
|
|
@@ -1086,7 +1092,14 @@ module CardDB
|
|
|
1086
1092
|
def completed_jobs = data['completedJobs']
|
|
1087
1093
|
def failed_jobs = data['failedJobs']
|
|
1088
1094
|
def failure_rate = data['failureRate']
|
|
1095
|
+
def no_match_count = data['noMatchCount']
|
|
1096
|
+
def no_match_rate = data['noMatchRate']
|
|
1089
1097
|
def average_shortlist_size = data['averageShortlistSize']
|
|
1098
|
+
def vector_query_count = data['vectorQueryCount']
|
|
1099
|
+
def vector_fallback_count = data['vectorFallbackCount']
|
|
1100
|
+
def average_vector_candidates = data['averageVectorCandidates']
|
|
1101
|
+
def average_vector_query_ms = data['averageVectorQueryMs']
|
|
1102
|
+
def average_embedding_ms = data['averageEmbeddingMs']
|
|
1090
1103
|
def confidence_distribution = data['confidenceDistribution'] || {}
|
|
1091
1104
|
|
|
1092
1105
|
def feedback
|
|
@@ -1948,91 +1961,12 @@ module CardDB
|
|
|
1948
1961
|
def updated_at = parse_time(data['updatedAt'])
|
|
1949
1962
|
end
|
|
1950
1963
|
|
|
1951
|
-
|
|
1952
|
-
class DeckPreviewToken < Resource
|
|
1953
|
-
def id
|
|
1954
|
-
data['id']
|
|
1955
|
-
end
|
|
1956
|
-
|
|
1957
|
-
def deck_id
|
|
1958
|
-
data['deckId']
|
|
1959
|
-
end
|
|
1960
|
-
|
|
1961
|
-
def label
|
|
1962
|
-
data['label']
|
|
1963
|
-
end
|
|
1964
|
-
|
|
1965
|
-
def expires_at
|
|
1966
|
-
parse_time(data['expiresAt'])
|
|
1967
|
-
end
|
|
1968
|
-
|
|
1969
|
-
def revoked_at
|
|
1970
|
-
parse_time(data['revokedAt'])
|
|
1971
|
-
end
|
|
1972
|
-
|
|
1973
|
-
def last_used_at
|
|
1974
|
-
parse_time(data['lastUsedAt'])
|
|
1975
|
-
end
|
|
1976
|
-
|
|
1977
|
-
def created_at
|
|
1978
|
-
parse_time(data['createdAt'])
|
|
1979
|
-
end
|
|
1980
|
-
|
|
1981
|
-
def updated_at
|
|
1982
|
-
parse_time(data['updatedAt'])
|
|
1983
|
-
end
|
|
1984
|
-
|
|
1985
|
-
private
|
|
1986
|
-
|
|
1987
|
-
def parse_time(value)
|
|
1988
|
-
return nil unless value
|
|
1989
|
-
|
|
1990
|
-
Time.parse(value)
|
|
1991
|
-
rescue ArgumentError
|
|
1992
|
-
value
|
|
1993
|
-
end
|
|
1994
|
-
end
|
|
1995
|
-
|
|
1996
|
-
# Wrapper for deck preview token creation payloads
|
|
1997
|
-
class DeckPreviewTokenCreatePayload < Resource
|
|
1998
|
-
def token
|
|
1999
|
-
data['token']
|
|
2000
|
-
end
|
|
2001
|
-
|
|
2002
|
-
def preview_token
|
|
2003
|
-
@preview_token ||= data['previewToken'] ? DeckPreviewToken.new(data['previewToken'], client: client) : nil
|
|
2004
|
-
end
|
|
2005
|
-
end
|
|
2006
|
-
|
|
2007
|
-
class DeckEmbedToken < Resource
|
|
1964
|
+
class DeckTokenIssuer < Resource
|
|
2008
1965
|
def id = data['id']
|
|
2009
|
-
def deck_id = data['deckId']
|
|
2010
1966
|
def api_application_id = data['apiApplicationId']
|
|
2011
1967
|
def label = data['label']
|
|
2012
|
-
def
|
|
2013
|
-
def
|
|
2014
|
-
def external_subject = data['externalSubject']
|
|
2015
|
-
def expires_at = parse_time(data['expiresAt'])
|
|
2016
|
-
def revoked_at = parse_time(data['revokedAt'])
|
|
2017
|
-
def last_used_at = parse_time(data['lastUsedAt'])
|
|
2018
|
-
def created_at = parse_time(data['createdAt'])
|
|
2019
|
-
def updated_at = parse_time(data['updatedAt'])
|
|
2020
|
-
end
|
|
2021
|
-
|
|
2022
|
-
class DeckEmbedTokenCreatePayload < Resource
|
|
2023
|
-
def token = data['token']
|
|
2024
|
-
|
|
2025
|
-
def embed_token
|
|
2026
|
-
@embed_token ||= data['embedToken'] ? DeckEmbedToken.new(data['embedToken'], client: client) : nil
|
|
2027
|
-
end
|
|
2028
|
-
end
|
|
2029
|
-
|
|
2030
|
-
class DeckAccessTokenIssuer < Resource
|
|
2031
|
-
def id = data['id']
|
|
2032
|
-
def deck_id = data['deckId']
|
|
2033
|
-
def api_application_id = data['apiApplicationId']
|
|
2034
|
-
def label = data['label']
|
|
2035
|
-
def read_modes = data['readModes'] || []
|
|
1968
|
+
def allowed_scopes = data['allowedScopes'] || []
|
|
1969
|
+
def allowed_representations = data['allowedRepresentations'] || []
|
|
2036
1970
|
def max_token_lifetime_seconds = data['maxTokenLifetimeSeconds']
|
|
2037
1971
|
def direct_signing_alg = data['directSigningAlg']
|
|
2038
1972
|
def direct_signing_key_id = data['directSigningKeyId']
|
|
@@ -2043,36 +1977,15 @@ module CardDB
|
|
|
2043
1977
|
def updated_at = parse_time(data['updatedAt'])
|
|
2044
1978
|
end
|
|
2045
1979
|
|
|
2046
|
-
class
|
|
1980
|
+
class DeckToken < Resource
|
|
2047
1981
|
def id = data['id']
|
|
2048
|
-
def deck_id = data['deckId']
|
|
2049
1982
|
def api_application_id = data['apiApplicationId']
|
|
2050
1983
|
def issuer_id = data['issuerId']
|
|
2051
|
-
def
|
|
2052
|
-
def external_subject = data['externalSubject']
|
|
2053
|
-
def expires_at = parse_time(data['expiresAt'])
|
|
2054
|
-
def revoked_at = parse_time(data['revokedAt'])
|
|
2055
|
-
def last_used_at = parse_time(data['lastUsedAt'])
|
|
2056
|
-
def created_at = parse_time(data['createdAt'])
|
|
2057
|
-
def updated_at = parse_time(data['updatedAt'])
|
|
2058
|
-
end
|
|
2059
|
-
|
|
2060
|
-
class DeckAccessTokenExchangePayload < Resource
|
|
2061
|
-
def token = data['token']
|
|
2062
|
-
|
|
2063
|
-
def access_token
|
|
2064
|
-
@access_token ||= data['accessToken'] ? DeckAccessToken.new(data['accessToken'], client: client) : nil
|
|
2065
|
-
end
|
|
2066
|
-
end
|
|
2067
|
-
|
|
2068
|
-
class DeckSessionToken < Resource
|
|
2069
|
-
def id = data['id']
|
|
2070
|
-
def account_id = data['accountId']
|
|
2071
|
-
def api_application_id = data['apiApplicationId']
|
|
1984
|
+
def deck_id = data['deckId']
|
|
2072
1985
|
def game_id = data['gameId']
|
|
2073
|
-
def environment = data['environment']
|
|
2074
1986
|
def external_subject = data['externalSubject']
|
|
2075
1987
|
def scopes = data['scopes'] || []
|
|
1988
|
+
def representation = data['representation']
|
|
2076
1989
|
def expires_at = parse_time(data['expiresAt'])
|
|
2077
1990
|
def revoked_at = parse_time(data['revokedAt'])
|
|
2078
1991
|
def last_used_at = parse_time(data['lastUsedAt'])
|
|
@@ -2080,11 +1993,11 @@ module CardDB
|
|
|
2080
1993
|
def updated_at = parse_time(data['updatedAt'])
|
|
2081
1994
|
end
|
|
2082
1995
|
|
|
2083
|
-
class
|
|
1996
|
+
class DeckTokenExchangePayload < Resource
|
|
2084
1997
|
def token = data['token']
|
|
2085
1998
|
|
|
2086
|
-
def
|
|
2087
|
-
@
|
|
1999
|
+
def deck_token
|
|
2000
|
+
@deck_token ||= data['deckToken'] ? DeckToken.new(data['deckToken'], client: client) : nil
|
|
2088
2001
|
end
|
|
2089
2002
|
end
|
|
2090
2003
|
|
data/lib/carddb/deck_tokens.rb
CHANGED
|
@@ -6,25 +6,19 @@ require 'json'
|
|
|
6
6
|
require 'openssl'
|
|
7
7
|
|
|
8
8
|
module CardDB
|
|
9
|
-
# Server-side helpers for minting direct signed deck
|
|
9
|
+
# Server-side helpers for minting direct signed deck tokens.
|
|
10
10
|
module DeckTokens
|
|
11
|
-
TOKEN_USE = 'carddb.
|
|
12
|
-
AUDIENCE = 'carddb.
|
|
11
|
+
TOKEN_USE = 'carddb.deck_token.v1'
|
|
12
|
+
AUDIENCE = 'carddb.deck'
|
|
13
13
|
ED25519_PKCS8_PREFIX = [
|
|
14
14
|
'302e020100300506032b657004220420'
|
|
15
15
|
].pack('H*').freeze
|
|
16
|
-
READ_MODES = {
|
|
17
|
-
'PUBLIC' => 'public',
|
|
18
|
-
'EMBED' => 'embed',
|
|
19
|
-
'FULL' => 'full'
|
|
20
|
-
}.freeze
|
|
21
|
-
private_constant :READ_MODES
|
|
22
16
|
|
|
23
17
|
class << self
|
|
24
|
-
# Generate an Ed25519 keypair for CardDB direct deck
|
|
18
|
+
# Generate an Ed25519 keypair for CardDB direct deck token signing.
|
|
25
19
|
#
|
|
26
20
|
# @return [Hash{Symbol => String}] Includes a PEM private key and the base64-encoded raw public key
|
|
27
|
-
def
|
|
21
|
+
def generate_direct_key_pair
|
|
28
22
|
signing_key = Ed25519::SigningKey.generate
|
|
29
23
|
|
|
30
24
|
{
|
|
@@ -33,43 +27,31 @@ module CardDB
|
|
|
33
27
|
}
|
|
34
28
|
end
|
|
35
29
|
|
|
36
|
-
# Sign a direct deck
|
|
30
|
+
# Sign a direct deck token that CardDB can verify with the issuer public key.
|
|
37
31
|
#
|
|
38
32
|
# @param issuer_id [String]
|
|
39
|
-
# @param deck_id [String]
|
|
40
33
|
# @param api_application_id [String]
|
|
41
34
|
# @param key_id [String]
|
|
42
35
|
# @param private_key [OpenSSL::PKey::PKey, String] Ed25519 private key as an OpenSSL key or PEM/DER string
|
|
43
|
-
# @param
|
|
36
|
+
# @param deck_id [String, nil]
|
|
37
|
+
# @param publisher_slug [String, nil]
|
|
38
|
+
# @param game_key [String, nil]
|
|
39
|
+
# @param external_subject [String, nil]
|
|
40
|
+
# @param scopes [Array<String>]
|
|
41
|
+
# @param representation [String, nil]
|
|
44
42
|
# @param subject [String, nil]
|
|
45
43
|
# @param token_id [String, nil]
|
|
46
44
|
# @param issued_at [Time]
|
|
47
45
|
# @param expires_at [Time]
|
|
48
46
|
# @return [String]
|
|
49
47
|
# @raise [ArgumentError]
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
issued_at: issued_at,
|
|
58
|
-
expires_at: expires_at
|
|
59
|
-
)
|
|
60
|
-
normalized_read_mode = normalize_read_mode!(read_mode)
|
|
61
|
-
key = normalize_private_key!(private_key)
|
|
62
|
-
header = { alg: 'EdDSA', kid: key_id.strip }
|
|
63
|
-
payload = build_payload(
|
|
64
|
-
issuer_id: issuer_id,
|
|
65
|
-
deck_id: deck_id,
|
|
66
|
-
api_application_id: api_application_id,
|
|
67
|
-
read_mode: normalized_read_mode,
|
|
68
|
-
subject: subject,
|
|
69
|
-
token_id: token_id,
|
|
70
|
-
issued_at: issued_at,
|
|
71
|
-
expires_at: expires_at
|
|
72
|
-
)
|
|
48
|
+
def sign_direct_token(**attributes)
|
|
49
|
+
attributes = validate_signing_input!(attributes)
|
|
50
|
+
normalized_scopes = normalize_scopes!(attributes[:scopes])
|
|
51
|
+
validate_token_target!(attributes)
|
|
52
|
+
key = normalize_private_key!(attributes[:private_key])
|
|
53
|
+
header = { alg: 'EdDSA', kid: attributes[:key_id].strip }
|
|
54
|
+
payload = build_payload(attributes.merge(scopes: normalized_scopes))
|
|
73
55
|
|
|
74
56
|
encoded_header = base64url_encode(JSON.generate(header))
|
|
75
57
|
encoded_payload = base64url_encode(JSON.generate(payload))
|
|
@@ -81,33 +63,52 @@ module CardDB
|
|
|
81
63
|
|
|
82
64
|
private
|
|
83
65
|
|
|
84
|
-
def validate_signing_input!(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
validate_required_string!(
|
|
88
|
-
validate_required_string!(
|
|
66
|
+
def validate_signing_input!(attributes)
|
|
67
|
+
validate_known_keys!(attributes)
|
|
68
|
+
attributes = { issued_at: Time.now }.merge(attributes)
|
|
69
|
+
validate_required_string!(attributes[:issuer_id], 'issuer_id')
|
|
70
|
+
validate_required_string!(attributes[:api_application_id], 'api_application_id')
|
|
71
|
+
validate_required_string!(attributes[:key_id], 'key_id')
|
|
89
72
|
|
|
90
|
-
issued_at = normalize_time!(issued_at, 'issued_at')
|
|
91
|
-
expires_at = normalize_time!(expires_at, 'expires_at')
|
|
73
|
+
issued_at = normalize_time!(attributes[:issued_at], 'issued_at')
|
|
74
|
+
expires_at = normalize_time!(attributes[:expires_at], 'expires_at')
|
|
92
75
|
raise ArgumentError, 'expires_at must be after issued_at' unless expires_at > issued_at
|
|
93
76
|
|
|
94
|
-
|
|
77
|
+
attributes.merge(issued_at: issued_at, expires_at: expires_at)
|
|
95
78
|
end
|
|
96
79
|
|
|
97
|
-
def
|
|
98
|
-
|
|
80
|
+
def validate_known_keys!(attributes)
|
|
81
|
+
allowed_keys = %i[
|
|
82
|
+
issuer_id api_application_id key_id private_key scopes expires_at deck_id publisher_slug game_key
|
|
83
|
+
external_subject representation subject token_id issued_at
|
|
84
|
+
]
|
|
85
|
+
unknown_keys = attributes.keys - allowed_keys
|
|
86
|
+
return if unknown_keys.empty?
|
|
87
|
+
|
|
88
|
+
raise ArgumentError, "unknown keyword: :#{unknown_keys.first}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_payload(attributes)
|
|
99
92
|
payload = {
|
|
100
93
|
token_use: TOKEN_USE,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
iss: issuer_id.strip,
|
|
94
|
+
app_id: attributes[:api_application_id].strip,
|
|
95
|
+
scopes: attributes[:scopes],
|
|
96
|
+
iss: attributes[:issuer_id].strip,
|
|
105
97
|
aud: AUDIENCE,
|
|
106
|
-
iat: issued_at.to_i,
|
|
107
|
-
exp: expires_at.to_i
|
|
98
|
+
iat: attributes[:issued_at].to_i,
|
|
99
|
+
exp: attributes[:expires_at].to_i
|
|
108
100
|
}
|
|
109
|
-
|
|
110
|
-
|
|
101
|
+
{
|
|
102
|
+
deck_id: :deck_id,
|
|
103
|
+
publisher_slug: :publisher_slug,
|
|
104
|
+
game_key: :game_key,
|
|
105
|
+
external_subject: :external_subject,
|
|
106
|
+
representation: :representation,
|
|
107
|
+
sub: :subject,
|
|
108
|
+
jti: :token_id
|
|
109
|
+
}.each do |claim, key|
|
|
110
|
+
add_optional_claim!(payload, claim, attributes[key])
|
|
111
|
+
end
|
|
111
112
|
payload
|
|
112
113
|
end
|
|
113
114
|
|
|
@@ -120,18 +121,32 @@ module CardDB
|
|
|
120
121
|
raise ArgumentError, "#{name} is required" unless value.is_a?(String) && !value.strip.empty?
|
|
121
122
|
end
|
|
122
123
|
|
|
124
|
+
def normalize_scopes!(scopes)
|
|
125
|
+
raise ArgumentError, 'scopes must include at least one DeckTokenScope' unless scopes.is_a?(Array) && !scopes.empty?
|
|
126
|
+
|
|
127
|
+
scopes.map do |scope|
|
|
128
|
+
validate_required_string!(scope, 'scope')
|
|
129
|
+
scope.strip
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def validate_token_target!(attributes)
|
|
134
|
+
return if string_present?(attributes[:deck_id])
|
|
135
|
+
return if %i[publisher_slug game_key external_subject].all? { |key| string_present?(attributes[key]) }
|
|
136
|
+
|
|
137
|
+
raise ArgumentError, 'deck_id or publisher_slug, game_key, and external_subject are required'
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def string_present?(value)
|
|
141
|
+
value&.strip&.length&.positive?
|
|
142
|
+
end
|
|
143
|
+
|
|
123
144
|
def normalize_time!(value, name)
|
|
124
145
|
raise ArgumentError, "#{name} must be a Time" unless value.is_a?(Time)
|
|
125
146
|
|
|
126
147
|
value
|
|
127
148
|
end
|
|
128
149
|
|
|
129
|
-
def normalize_read_mode!(read_mode)
|
|
130
|
-
READ_MODES.fetch(read_mode.to_s.upcase) do
|
|
131
|
-
raise ArgumentError, 'read_mode must be PUBLIC, EMBED, or FULL'
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
150
|
def normalize_private_key!(private_key)
|
|
136
151
|
return private_key if private_key.is_a?(OpenSSL::PKey::PKey)
|
|
137
152
|
|