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.
@@ -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
- # Wrapper for deck preview token metadata
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 read_mode = data['readMode']
2013
- def allowed_origins = data['allowedOrigins'] || []
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 DeckAccessToken < Resource
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 read_mode = data['readMode']
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 DeckSessionTokenExchangePayload < Resource
1996
+ class DeckTokenExchangePayload < Resource
2084
1997
  def token = data['token']
2085
1998
 
2086
- def session_token
2087
- @session_token ||= data['sessionToken'] ? DeckSessionToken.new(data['sessionToken'], client: client) : nil
1999
+ def deck_token
2000
+ @deck_token ||= data['deckToken'] ? DeckToken.new(data['deckToken'], client: client) : nil
2088
2001
  end
2089
2002
  end
2090
2003
 
@@ -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 access tokens.
9
+ # Server-side helpers for minting direct signed deck tokens.
10
10
  module DeckTokens
11
- TOKEN_USE = 'carddb.deck_access.v1'
12
- AUDIENCE = 'carddb.deck_access'
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 access token signing.
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 generate_direct_access_key_pair
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 access token that CardDB can verify with the issuer public key.
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 read_mode [String] PUBLIC, EMBED, or FULL
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 sign_direct_access_token(issuer_id:, deck_id:, api_application_id:, key_id:, private_key:, read_mode:,
51
- expires_at:, subject: nil, token_id: nil, issued_at: Time.now)
52
- issued_at, expires_at = validate_signing_input!(
53
- issuer_id: issuer_id,
54
- deck_id: deck_id,
55
- api_application_id: api_application_id,
56
- key_id: key_id,
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!(issuer_id:, deck_id:, api_application_id:, key_id:, issued_at:, expires_at:)
85
- validate_required_string!(issuer_id, 'issuer_id')
86
- validate_required_string!(deck_id, 'deck_id')
87
- validate_required_string!(api_application_id, 'api_application_id')
88
- validate_required_string!(key_id, 'key_id')
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
- [issued_at, expires_at]
77
+ attributes.merge(issued_at: issued_at, expires_at: expires_at)
95
78
  end
96
79
 
97
- def build_payload(issuer_id:, deck_id:, api_application_id:, read_mode:, subject:, token_id:, issued_at:,
98
- expires_at:)
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
- deck_id: deck_id.strip,
102
- app_id: api_application_id.strip,
103
- read_mode: read_mode,
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
- add_optional_claim!(payload, :sub, subject)
110
- add_optional_claim!(payload, :jti, token_id)
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