carddb 0.4.2 → 0.4.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.
@@ -1961,91 +1961,12 @@ module CardDB
1961
1961
  def updated_at = parse_time(data['updatedAt'])
1962
1962
  end
1963
1963
 
1964
- # Wrapper for deck preview token metadata
1965
- class DeckPreviewToken < Resource
1966
- def id
1967
- data['id']
1968
- end
1969
-
1970
- def deck_id
1971
- data['deckId']
1972
- end
1973
-
1974
- def label
1975
- data['label']
1976
- end
1977
-
1978
- def expires_at
1979
- parse_time(data['expiresAt'])
1980
- end
1981
-
1982
- def revoked_at
1983
- parse_time(data['revokedAt'])
1984
- end
1985
-
1986
- def last_used_at
1987
- parse_time(data['lastUsedAt'])
1988
- end
1989
-
1990
- def created_at
1991
- parse_time(data['createdAt'])
1992
- end
1993
-
1994
- def updated_at
1995
- parse_time(data['updatedAt'])
1996
- end
1997
-
1998
- private
1999
-
2000
- def parse_time(value)
2001
- return nil unless value
2002
-
2003
- Time.parse(value)
2004
- rescue ArgumentError
2005
- value
2006
- end
2007
- end
2008
-
2009
- # Wrapper for deck preview token creation payloads
2010
- class DeckPreviewTokenCreatePayload < Resource
2011
- def token
2012
- data['token']
2013
- end
2014
-
2015
- def preview_token
2016
- @preview_token ||= data['previewToken'] ? DeckPreviewToken.new(data['previewToken'], client: client) : nil
2017
- end
2018
- end
2019
-
2020
- class DeckEmbedToken < Resource
2021
- def id = data['id']
2022
- def deck_id = data['deckId']
2023
- def api_application_id = data['apiApplicationId']
2024
- def label = data['label']
2025
- def read_mode = data['readMode']
2026
- def allowed_origins = data['allowedOrigins'] || []
2027
- def external_subject = data['externalSubject']
2028
- def expires_at = parse_time(data['expiresAt'])
2029
- def revoked_at = parse_time(data['revokedAt'])
2030
- def last_used_at = parse_time(data['lastUsedAt'])
2031
- def created_at = parse_time(data['createdAt'])
2032
- def updated_at = parse_time(data['updatedAt'])
2033
- end
2034
-
2035
- class DeckEmbedTokenCreatePayload < Resource
2036
- def token = data['token']
2037
-
2038
- def embed_token
2039
- @embed_token ||= data['embedToken'] ? DeckEmbedToken.new(data['embedToken'], client: client) : nil
2040
- end
2041
- end
2042
-
2043
- class DeckAccessTokenIssuer < Resource
1964
+ class DeckTokenIssuer < Resource
2044
1965
  def id = data['id']
2045
- def deck_id = data['deckId']
2046
1966
  def api_application_id = data['apiApplicationId']
2047
1967
  def label = data['label']
2048
- def read_modes = data['readModes'] || []
1968
+ def allowed_scopes = data['allowedScopes'] || []
1969
+ def allowed_representations = data['allowedRepresentations'] || []
2049
1970
  def max_token_lifetime_seconds = data['maxTokenLifetimeSeconds']
2050
1971
  def direct_signing_alg = data['directSigningAlg']
2051
1972
  def direct_signing_key_id = data['directSigningKeyId']
@@ -2056,13 +1977,15 @@ module CardDB
2056
1977
  def updated_at = parse_time(data['updatedAt'])
2057
1978
  end
2058
1979
 
2059
- class DeckAccessToken < Resource
1980
+ class DeckToken < Resource
2060
1981
  def id = data['id']
2061
- def deck_id = data['deckId']
2062
1982
  def api_application_id = data['apiApplicationId']
2063
1983
  def issuer_id = data['issuerId']
2064
- def read_mode = data['readMode']
1984
+ def deck_id = data['deckId']
1985
+ def game_id = data['gameId']
2065
1986
  def external_subject = data['externalSubject']
1987
+ def scopes = data['scopes'] || []
1988
+ def representation = data['representation']
2066
1989
  def expires_at = parse_time(data['expiresAt'])
2067
1990
  def revoked_at = parse_time(data['revokedAt'])
2068
1991
  def last_used_at = parse_time(data['lastUsedAt'])
@@ -2070,34 +1993,122 @@ module CardDB
2070
1993
  def updated_at = parse_time(data['updatedAt'])
2071
1994
  end
2072
1995
 
2073
- class DeckAccessTokenExchangePayload < Resource
1996
+ class DeckTokenExchangePayload < Resource
2074
1997
  def token = data['token']
2075
1998
 
2076
- def access_token
2077
- @access_token ||= data['accessToken'] ? DeckAccessToken.new(data['accessToken'], client: client) : nil
1999
+ def deck_token
2000
+ @deck_token ||= data['deckToken'] ? DeckToken.new(data['deckToken'], client: client) : nil
2078
2001
  end
2079
2002
  end
2080
2003
 
2081
- class DeckSessionToken < Resource
2004
+ class Ruleset < Resource
2082
2005
  def id = data['id']
2083
- def account_id = data['accountId']
2084
- def api_application_id = data['apiApplicationId']
2085
2006
  def game_id = data['gameId']
2086
- def environment = data['environment']
2087
- def external_subject = data['externalSubject']
2088
- def scopes = data['scopes'] || []
2089
- def expires_at = parse_time(data['expiresAt'])
2090
- def revoked_at = parse_time(data['revokedAt'])
2091
- def last_used_at = parse_time(data['lastUsedAt'])
2007
+ def game = data['game']
2008
+ def key = data['key']
2009
+ def name = data['name']
2010
+ def description = data['description']
2011
+ def tags = data['tags'] || []
2012
+ def sort_order = data['sortOrder']
2013
+ def current_version_id = data['currentVersionId']
2014
+ def archived_at = parse_time(data['archivedAt'])
2015
+ def archived? = !!data['isArchived']
2092
2016
  def created_at = parse_time(data['createdAt'])
2093
2017
  def updated_at = parse_time(data['updatedAt'])
2018
+
2019
+ def current_version
2020
+ @current_version ||= data['currentVersion'] ? RulesetVersion.new(data['currentVersion'], client: client) : nil
2021
+ end
2094
2022
  end
2095
2023
 
2096
- class DeckSessionTokenExchangePayload < Resource
2097
- def token = data['token']
2024
+ class RulesetVersion < Resource
2025
+ def id = data['id']
2026
+ def ruleset_id = data['rulesetId']
2027
+ def version_label = data['versionLabel']
2028
+ def status = data['status']
2029
+ def rules = data['rules'] || {}
2030
+ def card_dataset_id = data['cardDatasetId']
2031
+ def card_dataset_version_id = data['cardDatasetVersionId']
2032
+ def source_dataset_id = data['sourceDatasetId']
2033
+ def source_dataset_version_id = data['sourceDatasetVersionId']
2034
+ def source_dataset_record_id = data['sourceDatasetRecordId']
2035
+ def source_record_identifier = data['sourceRecordIdentifier']
2036
+ def rules_fingerprint = data['rulesFingerprint']
2037
+ def effective_at = parse_time(data['effectiveAt'])
2038
+ def changelog = data['changelog']
2039
+ def created_by_account_id = data['createdByAccountId']
2040
+ def published_by_account_id = data['publishedByAccountId']
2041
+ def published_at = parse_time(data['publishedAt'])
2042
+ def superseded_at = parse_time(data['supersededAt'])
2043
+ def created_at = parse_time(data['createdAt'])
2044
+ def updated_at = parse_time(data['updatedAt'])
2045
+ def draft? = status == 'DRAFT'
2046
+ def active? = status == 'ACTIVE'
2047
+
2048
+ def section_definitions
2049
+ @section_definitions ||= (data['sectionDefinitions'] || []).map do |definition|
2050
+ DeckSectionDefinition.new(definition, client: client)
2051
+ end
2052
+ end
2053
+ end
2054
+
2055
+ class DeckValidationRules < Resource
2056
+ def ruleset_id = data['rulesetId']
2057
+ def ruleset_version_id = data['rulesetVersionId']
2058
+ def ruleset_key = data['rulesetKey']
2059
+ def ruleset_version_label = data['rulesetVersionLabel']
2060
+ def rules = data['rules'] || {}
2061
+
2062
+ def sections
2063
+ @sections ||= (data['sections'] || []).map { |section| DeckSectionDefinition.new(section, client: client) }
2064
+ end
2065
+ end
2066
+
2067
+ class RulesetValidationImpactDeck < Resource
2068
+ def deck_id = data['deckId']
2069
+ def title = data['title']
2070
+ def slug = data['slug']
2071
+ def state = data['state']
2072
+ def unpublished_changes? = !!data['hasUnpublishedChanges']
2073
+ def valid? = !!data['valid']
2074
+ def blocker_count = data['blockerCount']
2075
+ def warning_count = data['warningCount']
2076
+ def affected_entry_count = data['affectedEntryCount']
2077
+
2078
+ def issues
2079
+ @issues ||= (data['issues'] || []).map { |issue| DeckValidationIssue.new(issue, client: client) }
2080
+ end
2081
+ end
2082
+
2083
+ class RulesetValidationImpact < Resource
2084
+ def ruleset_id = data['rulesetId']
2085
+ def ruleset_version_id = data['rulesetVersionId']
2086
+ def checked_at = parse_time(data['checkedAt'])
2087
+ def affected_deck_count = data['affectedDeckCount']
2088
+ def invalid_deck_count = data['invalidDeckCount']
2089
+ def blocker_count = data['blockerCount']
2090
+ def warning_count = data['warningCount']
2098
2091
 
2099
- def session_token
2100
- @session_token ||= data['sessionToken'] ? DeckSessionToken.new(data['sessionToken'], client: client) : nil
2092
+ def results
2093
+ @results ||= (data['results'] || []).map { |deck| RulesetValidationImpactDeck.new(deck, client: client) }
2094
+ end
2095
+ end
2096
+
2097
+ class RulesetVersionDraftImportPreview < Resource
2098
+ def source_dataset_id = data['sourceDatasetId']
2099
+ def source_dataset_version_id = data['sourceDatasetVersionId']
2100
+ def source_dataset_record_id = data['sourceDatasetRecordId']
2101
+ def source_record_identifier = data['sourceRecordIdentifier']
2102
+ def rules = data['rules'] || {}
2103
+ def rules_fingerprint = data['rulesFingerprint']
2104
+ def summary = data['summary']
2105
+ def section_count = data['sectionCount']
2106
+ def copy_limit_count = data['copyLimitCount']
2107
+
2108
+ def section_definitions
2109
+ @section_definitions ||= (data['sectionDefinitions'] || []).map do |definition|
2110
+ DeckSectionDefinition.new(definition, client: client)
2111
+ end
2101
2112
  end
2102
2113
  end
2103
2114
 
@@ -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