health_cards 0.0.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6e94c49814ad651b6377277252fef1276ca99fa3b0704171f31571a986da62e
4
- data.tar.gz: 9a3a9b0e599a196d70d498089b23efb2479cece350f3bd1cb430ffbe4f22a32b
3
+ metadata.gz: 68a66d710cdd4d93e04b20f5dfdd9391523c3499b1d1ce2b006ed0bf5bb9ab65
4
+ data.tar.gz: 1b6d3a3c8819a1815aa4b9950610693b5f941c17ac568f5213c2735c776ba89f
5
5
  SHA512:
6
- metadata.gz: 39f9f579182341ec4124c991962ab8c7fcd8a09e5ced6e456a7f61248bc53c4a80b4b65a8eed2ba7756ecb29f8a68c8c8625017aad43a4cf38baa62ff3334194
7
- data.tar.gz: c9c486b517e6c4e07299662700c50e53be4286b870abeccc1114d042f216ba0489069770a1c574cdae3b6e852a07738749901109c84644ebba42869baf150b3f
6
+ metadata.gz: 8d4da867fb565f3cea48355d94edf36f30e6cccf59b15c85219b1aab54bf7b7172fe8482e7571a57a3322abe9cbaf11c69fe10350e3a94f60d87516b5ba9fb2b
7
+ data.tar.gz: e1ab50714275959ebcdef0065a2048d7fa0ec5147b2cb4aca0461999c4c3775eb118cc1c84b7a2167058e42288708495975a2df7ebf8df24a90d77b44b482736
@@ -9,9 +9,9 @@ module HealthCards
9
9
  base.extend ClassMethods
10
10
  end
11
11
 
12
- # Class level methods for HealthCard class specific settings
12
+ # Class level methods for Payload class specific settings
13
13
  module ClassMethods
14
- # Define allowed attributes for this HealthCard class
14
+ # Define allowed attributes for this Payload class
15
15
  # @param type [Class] Scopes the attributes to a spefic class. Must be a subclass of FHIR::Model
16
16
  # @param attributes [Array] An array of string with the attribute names that will be passed through
17
17
  # when data is minimized
@@ -19,7 +19,7 @@ module HealthCards
19
19
  allowable[type] = attributes
20
20
  end
21
21
 
22
- # Define disallowed attributes for this HealthCard class
22
+ # Define disallowed attributes for this Payload class
23
23
  # @param type [Class] Scopes the attributes to a spefic class. If not used will default to all FHIR resources.
24
24
  # To apply a rule to all FHIR types (resources and types), use FHIR::Model as the type
25
25
  # @param attributes [Array] An array of string with the attribute names that will be passed through
@@ -29,7 +29,7 @@ module HealthCards
29
29
  disallowable[type].concat(attributes)
30
30
  end
31
31
 
32
- # Define disallowed attributes for this HealthCard class
32
+ # Define disallowed attributes for this Payload class
33
33
  # @return [Hash] A hash of FHIR::Model subclasses and attributes that will pass through minimization
34
34
  def disallowable
35
35
  return @disallowable if @disallowable
@@ -37,7 +37,7 @@ module HealthCards
37
37
  @disallowable = parent_disallowables
38
38
  end
39
39
 
40
- # Define allowed attributes for this HealthCard class
40
+ # Define allowed attributes for this Payload class
41
41
  # @return [Hash] A hash of FHIR::Model subclasses and attributes that will pass through minimization
42
42
  def allowable
43
43
  return @allowable if @allowable
@@ -48,11 +48,11 @@ module HealthCards
48
48
  protected
49
49
 
50
50
  def parent_allowables(base = {})
51
- self < HealthCards::HealthCard ? base.merge(superclass.allowable) : base
51
+ self < HealthCards::Payload ? base.merge(superclass.allowable) : base
52
52
  end
53
53
 
54
54
  def parent_disallowables(base = {})
55
- self < HealthCards::HealthCard ? base.merge(superclass.disallowable) : base
55
+ self < HealthCards::Payload ? base.merge(superclass.disallowable) : base
56
56
  end
57
57
  end
58
58
 
@@ -61,7 +61,7 @@ module HealthCards
61
61
 
62
62
  return unless class_allowables
63
63
 
64
- allowed = resource.to_hash.select! { |att| class_allowables.include?(att) }
64
+ allowed = resource.to_hash.keep_if { |att| class_allowables.include?(att) }
65
65
 
66
66
  resource.from_hash(allowed)
67
67
  end
@@ -1,92 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Lint/MissingSuper
4
3
  module HealthCards
5
4
  # Represents a single QRCode in a sequence. This class is a shim to the RQRCode library
6
5
  # to enable multimode encoding
7
- class Chunk < RQRCode::QRCode
8
- attr_reader :ordinal
6
+ class Chunk
7
+ attr_reader :ordinal, :data, :qrcode
9
8
 
10
- def data
11
- @qrcode.data
12
- end
9
+ SINGLE_REGEX = %r{shc:/}.freeze
10
+ MULTI_REGEX = %r{shc:/[0-9]*/[0-9]*/}.freeze
13
11
 
14
- def initialize(ordinal: 1, input: nil)
12
+ def initialize(input:, ordinal: 1)
15
13
  @ordinal = ordinal
16
- @qrcode = ChunkCore.new(input)
17
- end
18
-
19
- def image
20
- as_png(
21
- border_modules: 1,
22
- module_px_size: 2
23
- )
24
- end
25
- end
26
-
27
- # RQRCodeCore shim for to enable multimode encoding
28
- class ChunkCore < RQRCodeCore::QRCode
29
- attr_accessor :data
30
-
31
- def initialize(input)
32
14
  @data = input
33
- @error_correct_level = 1
34
- @version = 22
35
- @module_count = @version * 4 + RQRCodeCore::QRPOSITIONPATTERNLENGTH
36
- @modules = Array.new(@module_count)
37
- @data_list = SHCQRCode.new(@data)
38
- @data_cache = nil
39
- make
40
- end
41
-
42
- # RQRCodeCore data shim for multimode encoding
43
- class SHCQRCode
44
- SINGLE_REGEX = %r{shc:/}.freeze
45
- MULTI_REGEX = %r{shc:/[0-9]*/[0-9]*/}.freeze
46
-
47
- def initialize(data)
48
- @data = data
49
- end
15
+ multi = MULTI_REGEX.match(input)
50
16
 
51
- def write(buffer)
52
- multi = MULTI_REGEX.match(@data)
53
- prefix = multi ? multi.to_s : SINGLE_REGEX.match(@data).to_s
17
+ prefix = multi ? multi.to_s : SINGLE_REGEX.match(input).to_s
18
+ content = input.delete_prefix(prefix)
54
19
 
55
- buffer.byte_encoding_start(prefix.length)
56
-
57
- prefix.each_byte do |b|
58
- buffer.put(b, 8)
59
- end
60
-
61
- num_content = @data.delete_prefix(prefix)
62
-
63
- buffer.numeric_encoding_start(num_content.length)
64
-
65
- num_content.size.times do |i|
66
- next unless (i % 3).zero?
67
-
68
- chars = num_content[i, 3]
69
- bit_length = get_bit_length(chars.length)
70
- buffer.put(get_code(chars), bit_length)
71
- end
72
- end
73
-
74
- private
75
-
76
- NUMBER_LENGTH = {
77
- 3 => 10,
78
- 2 => 7,
79
- 1 => 4
80
- }.freeze
20
+ @qrcode = RQRCode::QRCode.new([{ mode: :byte_8bit, data: prefix }, { mode: :number, data: content }],
21
+ max_size: 22, level: :l)
22
+ end
81
23
 
82
- def get_bit_length(length)
83
- NUMBER_LENGTH[length]
84
- end
24
+ def qr_code
25
+ @qrcode.qrcode
26
+ end
85
27
 
86
- def get_code(chars)
87
- chars.to_i
88
- end
28
+ def image
29
+ @qrcode.as_png(module_px_size: 2)
89
30
  end
90
31
  end
91
32
  end
92
- # rubocop:enable Lint/MissingSuper
@@ -25,7 +25,7 @@ module HealthCards
25
25
 
26
26
  class UnresolvableKeySetError < JWSError; end
27
27
 
28
- # Errors related to HealthCard / Bundle
28
+ # Errors related to Payload / Bundle
29
29
 
30
30
  # Exception thrown when an invalid payload is provided
31
31
  class InvalidPayloadError < HealthCardError
@@ -7,7 +7,7 @@ module HealthCards
7
7
  # Export JWS for file download
8
8
  # @param [Array<JWS, String>] An array of JWS objects to be exported
9
9
  # @return [String] JSON string containing file download contents
10
- def download(jws)
10
+ def file_download(jws)
11
11
  { verifiableCredential: jws.map(&:to_s) }.to_json
12
12
  end
13
13
 
@@ -1,203 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'zlib'
4
- require 'uri'
5
-
6
- require 'health_cards/attribute_filters'
7
- require 'health_cards/card_types'
8
-
9
3
  module HealthCards
10
- # A HealthCard which implements the credential claims specified by https://smarthealth.cards/
4
+ # Represents a signed SMART Health Card
11
5
  class HealthCard
12
- include HealthCards::AttributeFilters
13
- extend HealthCards::CardTypes
14
-
15
- FHIR_REF_REGEX = %r{((http|https)://([A-Za-z0-9\-\\.:%$]*/)+)?(
16
- Account|ActivityDefinition|AdverseEvent|AllergyIntolerance|Appointment|AppointmentResponse|AuditEvent|Basic|
17
- Binary|BiologicallyDerivedProduct|BodyStructure|Bundle|CapabilityStatement|CarePlan|CareTeam|CatalogEntry|
18
- ChargeItem|ChargeItemDefinition|Claim|ClaimResponse|ClinicalImpression|CodeSystem|Communication|
19
- CommunicationRequest|CompartmentDefinition|Composition|ConceptMap|Condition|Consent|Contract|Coverage|
20
- CoverageEligibilityRequest|CoverageEligibilityResponse|DetectedIssue|Device|DeviceDefinition|DeviceMetric
21
- |DeviceRequest|DeviceUseStatement|DiagnosticReport|DocumentManifest|DocumentReference|EffectEvidenceSynthesis|
22
- Encounter|Endpoint|EnrollmentRequest|EnrollmentResponse|EpisodeOfCare|EventDefinition|Evidence|EvidenceVariable|
23
- ExampleScenario|ExplanationOfBenefit|FamilyMemberHistory|Flag|Goal|GraphDefinition|Group|GuidanceResponse|
24
- HealthcareService|ImagingStudy|Immunization|ImmunizationEvaluation|ImmunizationRecommendation|
25
- ImplementationGuide|InsurancePlan|Invoice|Library|Linkage|List|Location|Measure|MeasureReport|Media|Medication|
26
- MedicationAdministration|MedicationDispense|MedicationKnowledge|MedicationRequest|MedicationStatement|
27
- MedicinalProduct|MedicinalProductAuthorization|MedicinalProductContraindication|MedicinalProductIndication|
28
- MedicinalProductIngredient|MedicinalProductInteraction|MedicinalProductManufactured|MedicinalProductPackaged|
29
- MedicinalProductPharmaceutical|MedicinalProductUndesirableEffect|MessageDefinition|MessageHeader|
30
- MolecularSequence|NamingSystem|NutritionOrder|Observation|ObservationDefinition|OperationDefinition|
31
- OperationOutcome|Organization|OrganizationAffiliation|Patient|PaymentNotice|PaymentReconciliation|Person|
32
- PlanDefinition|Practitioner|PractitionerRole|Procedure|Provenance|Questionnaire|QuestionnaireResponse|
33
- RelatedPerson|RequestGroup|ResearchDefinition|ResearchElementDefinition|ResearchStudy|ResearchSubject|
34
- RiskAssessment|RiskEvidenceSynthesis|Schedule|SearchParameter|ServiceRequest|Slot|Specimen|SpecimenDefinition|
35
- StructureDefinition|StructureMap|Subscription|Substance|SubstanceNucleicAcid|SubstancePolymer|SubstanceProtein|
36
- SubstanceReferenceInformation|SubstanceSourceMaterial|SubstanceSpecification|SupplyDelivery|SupplyRequest|Task|
37
- TerminologyCapabilities|TestReport|TestScript|ValueSet|VerificationResult|VisionPrescription)/
38
- [A-Za-z0-9\-.]{1,64}(/_history/[A-Za-z0-9\-.]{1,64})?}x.freeze
39
-
40
- attr_reader :issuer, :nbf, :bundle
41
-
42
- class << self
43
- # Creates a HealthCard from a JWS
44
- # @param jws [String] the JWS string
45
- # @param public_key [HealthCards::PublicKey] the public key associated with the JWS
46
- # @param key [HealthCards::PrivateKey] the private key associated with the JWS
47
- # @return [HealthCards::HealthCard]
48
- def from_jws(jws, public_key: nil, key: nil)
49
- jws = JWS.from_jws(jws, public_key: public_key, key: key)
50
- from_payload(jws.payload)
51
- end
52
-
53
- # Create a HealthCard from a compressed payload
54
- # @param payload [String]
55
- # @return [HealthCards::HealthCard]
56
- def from_payload(payload)
57
- json = decompress_payload(payload)
58
- bundle_hash = json.dig('vc', 'credentialSubject', 'fhirBundle')
59
-
60
- raise HealthCards::InvalidCredentialError unless bundle_hash
61
-
62
- bundle = FHIR::Bundle.new(bundle_hash)
63
- new(issuer: json['iss'], bundle: bundle)
64
- end
65
-
66
- # Decompress an arbitrary payload, useful for debugging
67
- # @param payload [String] compressed payload
68
- # @return [Hash] Hash built from JSON contents of payload
69
- def decompress_payload(payload)
70
- inf = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(payload)
71
- JSON.parse(inf)
72
- end
73
-
74
- # Compress an arbitrary payload, useful for debugging
75
- # @param payload [Object] Any object that responds to to_s
76
- # @return A compressed version of that payload parameter
77
- def compress_payload(payload)
78
- Zlib::Deflate.new(nil, -Zlib::MAX_WBITS).deflate(payload.to_s, Zlib::FINISH)
79
- end
80
-
81
- # Sets/Gets the fhir version that will be passed through to the credential created by an instnace of
82
- # this HealthCard (sub)class
83
- # @param ver [String] FHIR Version supported by this HealthCard (sub)class. Leaving this param out
84
- # will only return the current value
85
- # value (used as a getter)
86
- # @return [String] Current FHIR version supported
87
- def fhir_version(ver = nil)
88
- @fhir_version ||= ver unless ver.nil?
89
- @fhir_version
90
- end
91
- end
6
+ extend Forwardable
92
7
 
93
- allow type: FHIR::Meta, attributes: %w[security]
8
+ attr_reader :jws
94
9
 
95
- disallow attributes: %w[id text]
96
- disallow type: FHIR::CodeableConcept, attributes: %w[text]
97
- disallow type: FHIR::Coding, attributes: %w[display]
10
+ def_delegator :@qr_codes, :code_by_ordinal
11
+ def_delegators :@payload, :bundle, :issuer
98
12
 
99
- # Create a HealthCard
100
- #
101
- # @param bundle [FHIR::Bundle] VerifiableCredential containing a fhir bundle
102
- # @param issuer [String] The url from the Issuer of the HealthCard
103
- def initialize(bundle:, issuer: nil)
104
- raise InvalidPayloadError unless bundle.is_a?(FHIR::Bundle) # && bundle.valid?
105
-
106
- @issuer = issuer
107
- @bundle = bundle
13
+ # Create a HealthCard from a JWS
14
+ # @param jws [JWS, String] A JWS object or JWS string
15
+ def initialize(jws)
16
+ @jws = JWS.from_jws(jws)
17
+ @payload = Payload.from_payload(@jws.payload)
18
+ @qr_codes = QRCodes.from_jws(@jws)
108
19
  end
109
20
 
110
- # A Hash matching the VC structure specified by https://smarthealth.cards/#health-cards-are-encoded-as-compact-serialization-json-web-signatures-jws
111
- # @return [Hash]
112
- def to_hash
113
- {
114
- iss: issuer,
115
- nbf: Time.now.to_i,
116
- vc: {
117
- type: self.class.types,
118
- credentialSubject: {
119
- fhirVersion: self.class.fhir_version,
120
- fhirBundle: strip_fhir_bundle.to_hash
121
- }
122
- }
123
- }
21
+ # Export HealthCard as JSON, formatted for file downloads
22
+ # @return [String] JSON string containing file download contents
23
+ def to_json(*_args)
24
+ Exporter.file_download([@jws])
124
25
  end
125
26
 
126
- # A compressed version of the FHIR::Bundle based on the SMART Health Cards frame work and any other constraints
127
- # defined by a subclass
128
- # @return String compressed payload
129
- def to_s
130
- HealthCard.compress_payload(to_json)
27
+ # QR Codes representing this HealthCard
28
+ # @return [Array<Chunk>] an array of QR Code chunks
29
+ def qr_codes
30
+ @qr_codes.chunks
131
31
  end
132
32
 
133
- # A minified JSON string matching the VC structure specified by https://smarthealth.cards/#health-cards-are-encoded-as-compact-serialization-json-web-signatures-jws
134
- # @return [String] JSON string
135
- def to_json(*_args)
136
- to_hash.to_json
33
+ # Extracts a resource from the bundle contained in the HealthCard. A filter
34
+ # can be applied by using a block. The method will yield each resource to the block.
35
+ # The block should return a boolean
36
+ # @param type [Class] :type should be a class representing a FHIR resource
37
+ # @return The first bundle resource that matches the type and/or block evaluation
38
+ def resource(type: nil, &block)
39
+ resources(type: type, &block).first
137
40
  end
138
41
 
139
- # Processes the bundle according to https://smarthealth.cards/#health-cards-are-small and returns
140
- # a Hash with equivalent values
141
- # @return [Hash] A hash with the same content as the FHIR::Bundle, processed accoding
142
- # to SMART Health Cards framework and any constraints created by subclasses
143
- def strip_fhir_bundle
144
- return [] unless bundle.entry
145
-
146
- new_bundle = duplicate_bundle
147
- url_map = redefine_uris(new_bundle)
148
-
149
- new_bundle.entry.each do |entry|
150
- entry.each_element do |value, metadata, _|
151
- case metadata['type']
152
- when 'Reference'
153
- value.reference = process_reference(url_map, entry, value)
154
- when 'Resource'
155
- value.meta = nil unless value.meta&.security
156
- end
157
-
158
- handle_allowable(value)
159
- handle_disallowable(value)
160
- end
42
+ # Extracts all resources from the bundle contained in the HealthCard. A filter
43
+ # can be applied by using a block. The method will yield each resource to the block.
44
+ # The block should return a boolean
45
+ # @param type [Class] :type should be a class representing a FHIR resource
46
+ # @return The first bundle resource that matches the type and/or block evaluation
47
+ def resources(type: nil, &block)
48
+ all_resources = bundle.entry.map(&:resource)
49
+ return all_resources unless type || block
50
+
51
+ all_resources.filter do |r|
52
+ resource_matches_criteria(r, type, &block)
161
53
  end
162
-
163
- new_bundle
164
54
  end
165
55
 
166
56
  private
167
57
 
168
- def duplicate_bundle
169
- FHIR::Bundle.new(bundle.to_hash)
170
- end
171
-
172
- def redefine_uris(bundle)
173
- url_map = {}
174
- resource_count = 0
175
- bundle.entry.each do |entry|
176
- old_url = entry.fullUrl
177
- new_url = "resource:#{resource_count}"
178
- url_map[old_url] = new_url
179
- entry.fullUrl = new_url
180
- resource_count += 1
58
+ def resource_matches_criteria(resource, type, &block)
59
+ of_type = type && resource.is_a?(type)
60
+ if block && type
61
+ of_type && yield(resource)
62
+ elsif !type && block
63
+ yield(resource)
64
+ else
65
+ of_type
181
66
  end
182
- url_map
183
- end
184
-
185
- def process_reference(url_map, entry, ref)
186
- entry_url = URI(url_map.key(entry.fullUrl))
187
- ref_url = ref.reference
188
-
189
- return unless ref_url
190
-
191
- return url_map[ref_url] if url_map[ref_url]
192
-
193
- fhir_base_url = FHIR_REF_REGEX.match(entry_url.to_s)[1]
194
- full_url = URI.join(fhir_base_url, ref_url).to_s
195
-
196
- new_url = url_map[full_url]
197
-
198
- raise InvalidBundleReferenceError, full_url unless new_url
199
-
200
- new_url
201
67
  end
202
68
  end
203
69
  end
@@ -27,7 +27,7 @@ module HealthCards
27
27
  # @return [Hash] Hash containing the JWS payload and verification contents
28
28
  private_class_method def self.verify_jws(jws_string)
29
29
  jws = JWS.from_jws jws_string
30
- result = { payload: HealthCard.decompress_payload(jws.payload) }
30
+ result = { payload: Payload.decompress_payload(jws.payload) }
31
31
  begin
32
32
  result[:verified] = Verifier.verify jws
33
33
  result[:error_message] = 'Signature Invalid' if result[:verified] == false
@@ -16,11 +16,11 @@ module HealthCards
16
16
  self.key = key
17
17
  end
18
18
 
19
- # Create a HealthCard from the supplied FHIR bundle
19
+ # Create a Payload from the supplied FHIR bundle
20
20
  #
21
21
  # @param bundle [FHIR::Bundle, String] the FHIR bundle used as the Health Card payload
22
- # @return [HealthCard::]
23
- def create_health_card(bundle, type: HealthCard)
22
+ # @return [Payload::]
23
+ def create_payload(bundle, type: Payload)
24
24
  type.new(issuer: url, bundle: bundle)
25
25
  end
26
26
 
@@ -30,11 +30,16 @@ module HealthCards
30
30
  # @param type [Class] A subclass of HealthCards::Card that processes the bundle according to a specific IG.
31
31
  # Leave blank for default SMART Health Card behavior
32
32
  # @return [HealthCards::JWS] An instance of JWS using the payload and signed by the issuer's private key
33
- def issue_jws(bundle, type: HealthCard)
34
- card = create_health_card(bundle, type: type)
33
+ def issue_jws(bundle, type: Payload)
34
+ card = create_payload(bundle, type: type)
35
35
  JWS.new(header: jws_header, payload: card.to_s, key: key)
36
36
  end
37
37
 
38
+ def issue_health_card(bundle, type: Payload)
39
+ jws = issue_jws(bundle, type: type)
40
+ HealthCards::HealthCard.new(jws)
41
+ end
42
+
38
43
  # Set the private key used for signing issued health cards
39
44
  #
40
45
  # @param key [HealthCards::PrivateKey, nil] the private key used for signing issued health cards
@@ -6,12 +6,15 @@ module HealthCards
6
6
  class << self
7
7
  include Encoding
8
8
 
9
- # Creates a Card from a JWS
10
- # @param jws [String, HealthCards::JWS] the JWS string
9
+ # Creates a JWS from a String representation, or returns the HealthCards::JWS object
10
+ # that was passed in
11
+ # @param jws [String, HealthCards::JWS] the JWS string, or a JWS object
11
12
  # @param public_key [HealthCards::PublicKey] the public key associated with the JWS
12
13
  # @param key [HealthCards::PrivateKey] the private key associated with the JWS
13
- # @return [HealthCards::HealthCard]
14
+ # @return [HealthCards::JWS] A new JWS object, or the JWS object that was passed in
14
15
  def from_jws(jws, public_key: nil, key: nil)
16
+ return jws if jws.is_a?(HealthCards::JWS) && public_key.nil? && key.nil?
17
+
15
18
  unless jws.is_a?(HealthCards::JWS) || jws.is_a?(String)
16
19
  raise ArgumentError,
17
20
  'Expected either a HealthCards::JWS or String'
@@ -44,7 +44,7 @@ module HealthCards
44
44
  # Retrieves a key from the keyset with a kid
45
45
  # that matches the parameter
46
46
  # @param kid [String] a Base64 encoded kid from a JWS or Key
47
- # @return [HealthCard::Key] a key with a matching kid or nil if not found
47
+ # @return [Payload::Key] a key with a matching kid or nil if not found
48
48
  def find_key(kid)
49
49
  @key_map[kid]
50
50
  end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zlib'
4
+ require 'uri'
5
+
6
+ require 'health_cards/attribute_filters'
7
+ require 'health_cards/payload_types'
8
+
9
+ module HealthCards
10
+ # A Payload which implements the credential claims specified by https://smarthealth.cards/
11
+ class Payload
12
+ include HealthCards::AttributeFilters
13
+ extend HealthCards::PayloadTypes
14
+
15
+ FHIR_REF_REGEX = %r{((http|https)://([A-Za-z0-9\-\\.:%$]*/)+)?(
16
+ Account|ActivityDefinition|AdverseEvent|AllergyIntolerance|Appointment|AppointmentResponse|AuditEvent|Basic|
17
+ Binary|BiologicallyDerivedProduct|BodyStructure|Bundle|CapabilityStatement|CarePlan|CareTeam|CatalogEntry|
18
+ ChargeItem|ChargeItemDefinition|Claim|ClaimResponse|ClinicalImpression|CodeSystem|Communication|
19
+ CommunicationRequest|CompartmentDefinition|Composition|ConceptMap|Condition|Consent|Contract|Coverage|
20
+ CoverageEligibilityRequest|CoverageEligibilityResponse|DetectedIssue|Device|DeviceDefinition|DeviceMetric
21
+ |DeviceRequest|DeviceUseStatement|DiagnosticReport|DocumentManifest|DocumentReference|EffectEvidenceSynthesis|
22
+ Encounter|Endpoint|EnrollmentRequest|EnrollmentResponse|EpisodeOfCare|EventDefinition|Evidence|EvidenceVariable|
23
+ ExampleScenario|ExplanationOfBenefit|FamilyMemberHistory|Flag|Goal|GraphDefinition|Group|GuidanceResponse|
24
+ HealthcareService|ImagingStudy|Immunization|ImmunizationEvaluation|ImmunizationRecommendation|
25
+ ImplementationGuide|InsurancePlan|Invoice|Library|Linkage|List|Location|Measure|MeasureReport|Media|Medication|
26
+ MedicationAdministration|MedicationDispense|MedicationKnowledge|MedicationRequest|MedicationStatement|
27
+ MedicinalProduct|MedicinalProductAuthorization|MedicinalProductContraindication|MedicinalProductIndication|
28
+ MedicinalProductIngredient|MedicinalProductInteraction|MedicinalProductManufactured|MedicinalProductPackaged|
29
+ MedicinalProductPharmaceutical|MedicinalProductUndesirableEffect|MessageDefinition|MessageHeader|
30
+ MolecularSequence|NamingSystem|NutritionOrder|Observation|ObservationDefinition|OperationDefinition|
31
+ OperationOutcome|Organization|OrganizationAffiliation|Patient|PaymentNotice|PaymentReconciliation|Person|
32
+ PlanDefinition|Practitioner|PractitionerRole|Procedure|Provenance|Questionnaire|QuestionnaireResponse|
33
+ RelatedPerson|RequestGroup|ResearchDefinition|ResearchElementDefinition|ResearchStudy|ResearchSubject|
34
+ RiskAssessment|RiskEvidenceSynthesis|Schedule|SearchParameter|ServiceRequest|Slot|Specimen|SpecimenDefinition|
35
+ StructureDefinition|StructureMap|Subscription|Substance|SubstanceNucleicAcid|SubstancePolymer|SubstanceProtein|
36
+ SubstanceReferenceInformation|SubstanceSourceMaterial|SubstanceSpecification|SupplyDelivery|SupplyRequest|Task|
37
+ TerminologyCapabilities|TestReport|TestScript|ValueSet|VerificationResult|VisionPrescription)/
38
+ [A-Za-z0-9\-.]{1,64}(/_history/[A-Za-z0-9\-.]{1,64})?}x.freeze
39
+
40
+ attr_reader :issuer, :nbf, :bundle
41
+
42
+ class << self
43
+ # Create a Payload from a compressed payload
44
+ # @param payload [String]
45
+ # @return [HealthCards::Payload]
46
+ def from_payload(payload)
47
+ json = decompress_payload(payload)
48
+ bundle_hash = json.dig('vc', 'credentialSubject', 'fhirBundle')
49
+
50
+ raise HealthCards::InvalidCredentialError unless bundle_hash
51
+
52
+ bundle = extract_bundle(payload)
53
+ new(issuer: json['iss'], bundle: bundle)
54
+ end
55
+
56
+ # Extract a bundle from a compressed payload
57
+ # @param payload [String]
58
+ # @return [FHIR::Bundle]
59
+ def extract_bundle(payload)
60
+ json = decompress_payload(payload)
61
+ bundle_hash = json.dig('vc', 'credentialSubject', 'fhirBundle')
62
+
63
+ raise HealthCards::InvalidCredentialError unless bundle_hash
64
+
65
+ FHIR::Bundle.new(bundle_hash)
66
+ end
67
+
68
+ # Decompress an arbitrary payload, useful for debugging
69
+ # @param payload [String] compressed payload
70
+ # @return [Hash] Hash built from JSON contents of payload
71
+ def decompress_payload(payload)
72
+ inf = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(payload)
73
+ JSON.parse(inf)
74
+ end
75
+
76
+ # Compress an arbitrary payload, useful for debugging
77
+ # @param payload [Object] Any object that responds to to_s
78
+ # @return A compressed version of that payload parameter
79
+ def compress_payload(payload)
80
+ Zlib::Deflate.new(nil, -Zlib::MAX_WBITS).deflate(payload.to_s, Zlib::FINISH)
81
+ end
82
+
83
+ # Sets/Gets the fhir version that will be passed through to the credential created by an instnace of
84
+ # this Payload (sub)class
85
+ # @param ver [String] FHIR Version supported by this Payload (sub)class. Leaving this param out
86
+ # will only return the current value
87
+ # value (used as a getter)
88
+ # @return [String] Current FHIR version supported
89
+ def fhir_version(ver = nil)
90
+ if @fhir_version.nil? && ver.nil?
91
+ @fhir_version = superclass.fhir_version unless self == HealthCards::Payload
92
+ elsif ver
93
+ @fhir_version = ver
94
+ end
95
+ @fhir_version
96
+ end
97
+ end
98
+
99
+ fhir_version '4.0.1'
100
+
101
+ additional_types 'https://smarthealth.cards#health-card'
102
+
103
+ allow type: FHIR::Meta, attributes: %w[security]
104
+
105
+ disallow attributes: %w[id text]
106
+ disallow type: FHIR::CodeableConcept, attributes: %w[text]
107
+ disallow type: FHIR::Coding, attributes: %w[display]
108
+
109
+ # Create a Payload
110
+ #
111
+ # @param bundle [FHIR::Bundle] VerifiableCredential containing a fhir bundle
112
+ # @param issuer [String] The url from the Issuer of the Payload
113
+ def initialize(bundle:, issuer: nil)
114
+ raise InvalidPayloadError unless bundle.is_a?(FHIR::Bundle) # && bundle.valid?
115
+
116
+ @issuer = issuer
117
+ @bundle = bundle
118
+ end
119
+
120
+ # A Hash matching the VC structure specified by https://smarthealth.cards/#health-cards-are-encoded-as-compact-serialization-json-web-signatures-jws
121
+ # @param filter [Boolean] specifies whether the bundle should apply allow/disallow rules
122
+ # and meta filtering features. Defaults to true.
123
+ # @return [Hash]
124
+ def to_hash(filter: true)
125
+ fhir_bundle = filter ? strip_fhir_bundle : bundle
126
+ {
127
+ iss: issuer,
128
+ nbf: Time.now.to_i,
129
+ vc: {
130
+ type: self.class.types,
131
+ credentialSubject: {
132
+ fhirVersion: self.class.fhir_version,
133
+ fhirBundle: fhir_bundle.to_hash
134
+ }
135
+ }
136
+ }
137
+ end
138
+
139
+ # A compressed version of the FHIR::Bundle based on the SMART Health Cards frame work and any other constraints
140
+ # defined by a subclass
141
+ # @return String compressed payload
142
+ def to_s
143
+ Payload.compress_payload(to_json)
144
+ end
145
+
146
+ # A minified JSON string matching the VC structure specified by https://smarthealth.cards/#health-cards-are-encoded-as-compact-serialization-json-web-signatures-jws
147
+ # @return [String] JSON string
148
+ def to_json(*args)
149
+ to_hash.to_json(*args)
150
+ end
151
+
152
+ # Processes the bundle according to https://smarthealth.cards/#health-cards-are-small and returns
153
+ # a Hash with equivalent values
154
+ # @return [Hash] A hash with the same content as the FHIR::Bundle, processed accoding
155
+ # to SMART Health Cards framework and any constraints created by subclasses
156
+ def strip_fhir_bundle
157
+ return [] unless bundle.entry
158
+
159
+ new_bundle = duplicate_bundle
160
+ url_map = redefine_uris(new_bundle)
161
+
162
+ new_bundle.entry.each do |entry|
163
+ entry.each_element do |value, metadata, _|
164
+ case metadata['type']
165
+ when 'Reference'
166
+ value.reference = process_reference(url_map, entry, value)
167
+ when 'Resource'
168
+ value.meta = nil unless value.meta&.security
169
+ end
170
+
171
+ handle_allowable(value)
172
+ handle_disallowable(value)
173
+ end
174
+ end
175
+
176
+ new_bundle
177
+ end
178
+
179
+ private
180
+
181
+ def duplicate_bundle
182
+ FHIR::Bundle.new(bundle.to_hash)
183
+ end
184
+
185
+ def redefine_uris(bundle)
186
+ url_map = {}
187
+ resource_count = 0
188
+ bundle.entry.each do |entry|
189
+ old_url = entry.fullUrl
190
+ new_url = "resource:#{resource_count}"
191
+ url_map[old_url] = new_url
192
+ entry.fullUrl = new_url
193
+ resource_count += 1
194
+ end
195
+ url_map
196
+ end
197
+
198
+ def process_reference(url_map, entry, ref)
199
+ entry_url = URI(url_map.key(entry.fullUrl))
200
+ ref_url = ref.reference
201
+
202
+ return unless ref_url
203
+
204
+ return url_map[ref_url] if url_map[ref_url]
205
+
206
+ fhir_base_url = FHIR_REF_REGEX.match(entry_url.to_s)[1]
207
+ full_url = URI.join(fhir_base_url, ref_url).to_s
208
+
209
+ new_url = url_map[full_url]
210
+
211
+ raise InvalidBundleReferenceError, full_url unless new_url
212
+
213
+ new_url
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HealthCards
4
+ class COVIDImmunizationPayload < COVIDPayload
5
+ additional_types 'https://smarthealth.cards#immunization'
6
+
7
+ allow type: FHIR::Immunization,
8
+ attributes: %w[meta status vaccineCode patient occurrenceDateTime manufacturer lotNumber performer
9
+ isSubpotent]
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HealthCards
4
+ class COVIDLabResultPayload < COVIDPayload
5
+ additional_types 'https://smarthealth.cards#laboratory'
6
+
7
+ allow type: FHIR::Observation,
8
+ attributes: %w[meta status code subject effectiveDateTime effectivePeriod performer
9
+ valueCodeableConcept valueQuantity valueString referenceRange]
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'health_cards/attribute_filters'
4
+ require 'health_cards/payload_types'
5
+
6
+ module HealthCards
7
+ # Implements Payload for use with COVID Vaccination IG
8
+ class COVIDPayload < HealthCards::Payload
9
+ additional_types 'https://smarthealth.cards#covid19'
10
+
11
+ allow type: FHIR::Patient, attributes: %w[identifier name birthDate]
12
+ end
13
+ end
@@ -1,30 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HealthCards
4
- # Handles behavior related to support types by Healthcard subclasses
5
- module CardTypes
6
- VC_TYPE = [
7
- 'https://smarthealth.cards#health-card'
8
- ].freeze
9
-
10
- # Additional type claims this HealthCard class supports
4
+ # Handles behavior related to support types by Payload subclasses
5
+ module PayloadTypes
6
+ # Additional type claims this Payload class supports
11
7
  # @param types [String, Array] A string or array of string representing the additional type claims or nil
12
8
  # if used as a getter
13
9
  # @return [Array] the additional types added by this classes
14
10
  def additional_types(*add_types)
15
- types.concat(add_types) unless add_types.nil?
16
- types - VC_TYPE
11
+ @additional_types ||= []
12
+ @additional_types.concat(add_types) unless add_types.nil?
13
+ @additional_types
17
14
  end
18
15
 
19
- # Type claims supported by this HealthCard subclass
16
+ # Type claims supported by this Payload subclass
20
17
  # @return [Array] an array of Strings with all the supported type claims
21
18
  def types
22
- @types ||= VC_TYPE.dup
19
+ @types ||= self == HealthCards::Payload ? additional_types : superclass.types + additional_types
23
20
  end
24
21
 
25
22
  # Check if this class supports the given type claim(s)
26
23
  # @param type [Array, String] A type as defined by the SMART Health Cards framework
27
- # @return [Boolean] Whether or not the type param is included in the types supported by the HealthCard (sub)class
24
+ # @return [Boolean] Whether or not the type param is included in the types supported by the Payload (sub)class
28
25
  def supports_type?(*type)
29
26
  !types.intersection(type.flatten).empty?
30
27
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HealthCards
4
- # Logic for verifying a HealthCard JWS
4
+ # Logic for verifying a Payload JWS
5
5
  module Verification
6
6
  # Verify Health Card with given KeySet
7
7
  #
@@ -10,7 +10,7 @@ module HealthCards
10
10
  # @param resolve_keys [Boolean] if keys should be resolved
11
11
  # @return [Boolean]
12
12
  def verify_using_key_set(verifiable, key_set = nil, resolve_keys: true)
13
- jws = JWS.from_jws(verifiable)
13
+ jws = verifiable.is_a?(HealthCards::HealthCard) ? verifiable.jws : JWS.from_jws(verifiable)
14
14
  key_set ||= HealthCards::KeySet.new
15
15
  key_set.add_keys(resolve_key(jws)) if resolve_keys && key_set.find_key(jws.kid).nil?
16
16
 
@@ -28,7 +28,7 @@ module HealthCards
28
28
  # @param jws [HealthCards::JWS, String] The JWS for which to resolve keys
29
29
  # @return [HealthCards::KeySet]
30
30
  def resolve_key(jws)
31
- jwks_uri = URI("#{HealthCard.from_jws(jws.to_s).issuer}/.well-known/jwks.json")
31
+ jwks_uri = URI("#{HealthCard.new(jws.to_s).issuer}/.well-known/jwks.json")
32
32
  res = Net::HTTP.get(jwks_uri)
33
33
  HealthCards::KeySet.from_jwks(res)
34
34
  # Handle response if key is malformed or unreachable
@@ -12,7 +12,7 @@ module HealthCards
12
12
  include HealthCards::Verification
13
13
  extend HealthCards::Verification
14
14
 
15
- # Verify a HealthCard
15
+ # Verify a Payload
16
16
  #
17
17
  # This method _always_ uses key resolution and does not depend on any cached keys
18
18
  #
@@ -53,7 +53,7 @@ module HealthCards
53
53
  @keys.remove_keys(key)
54
54
  end
55
55
 
56
- # Verify a HealthCard
56
+ # Verify a Payload
57
57
  #
58
58
  # @param verifiable [HealthCards::JWS, String] the health card to verify
59
59
  # @return [Boolean]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HealthCards
4
- VERSION = '0.0.2'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/health_cards.rb CHANGED
@@ -9,15 +9,18 @@ require 'health_cards/key_set'
9
9
  require 'health_cards/private_key'
10
10
  require 'health_cards/public_key'
11
11
  require 'health_cards/health_card'
12
+ require 'health_cards/payload'
12
13
  require 'health_cards/attribute_filters'
13
- require 'health_cards/card_types'
14
+ require 'health_cards/payload_types'
14
15
  require 'health_cards/chunking_utils'
15
16
  require 'health_cards/errors'
16
17
  require 'health_cards/qr_codes'
17
18
  require 'health_cards/chunk'
18
19
  require 'health_cards/verifier'
19
20
  require 'health_cards/importer'
20
- require 'health_cards/covid_health_card'
21
+ require 'health_cards/payload_types/covid_payload'
22
+ require 'health_cards/payload_types/covid_immunization_payload'
23
+ require 'health_cards/payload_types/covid_lab_result_payload'
21
24
  require 'health_cards/exporter'
22
25
 
23
26
  require 'base64'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: health_cards
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reece Adamson
@@ -15,7 +15,7 @@ authors:
15
15
  autorequire:
16
16
  bindir: exe
17
17
  cert_chain: []
18
- date: 2021-07-09 00:00:00.000000000 Z
18
+ date: 2021-11-30 00:00:00.000000000 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: fhir_models
@@ -45,14 +45,28 @@ dependencies:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
+ - !ruby/object:Gem::Dependency
49
+ name: rqrcode_core
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 1.2.0
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 1.2.0
48
62
  description: "Health Cards implements SMART Health Cards, a secure and decentralized
49
63
  framework that allows \npeople to prove their vaccination status or medical test
50
- results. It is built on top of FHIR R4 \nhealth data exchange standards and W3C
51
- Verifiable Credentials. It allows conversion of\nclinical data into JWS which may
52
- then be embedded into QR codes, exported to smart-health-card files, or\nreturned
53
- by a $health-card-issue FHIR operation.\n"
64
+ results. It is based off of W3C \nVerifiable Credentials and FHIR R4 health data
65
+ exchange standards. It allows conversion of \nclinical data into JWS which may then
66
+ be embedded into QR codes, exported to smart-health-card \nfiles, or returned by
67
+ a $health-card-issue FHIR operation.\n"
54
68
  email:
55
- - radamson@mitre.org
69
+ - vci-developers@mitre.org
56
70
  executables: []
57
71
  extensions: []
58
72
  extra_rdoc_files: []
@@ -60,10 +74,8 @@ files:
60
74
  - LICENSE.txt
61
75
  - lib/health_cards.rb
62
76
  - lib/health_cards/attribute_filters.rb
63
- - lib/health_cards/card_types.rb
64
77
  - lib/health_cards/chunk.rb
65
78
  - lib/health_cards/chunking_utils.rb
66
- - lib/health_cards/covid_health_card.rb
67
79
  - lib/health_cards/encoding.rb
68
80
  - lib/health_cards/errors.rb
69
81
  - lib/health_cards/exporter.rb
@@ -73,19 +85,24 @@ files:
73
85
  - lib/health_cards/jws.rb
74
86
  - lib/health_cards/key.rb
75
87
  - lib/health_cards/key_set.rb
88
+ - lib/health_cards/payload.rb
89
+ - lib/health_cards/payload_types.rb
90
+ - lib/health_cards/payload_types/covid_immunization_payload.rb
91
+ - lib/health_cards/payload_types/covid_lab_result_payload.rb
92
+ - lib/health_cards/payload_types/covid_payload.rb
76
93
  - lib/health_cards/private_key.rb
77
94
  - lib/health_cards/public_key.rb
78
95
  - lib/health_cards/qr_codes.rb
79
96
  - lib/health_cards/verification.rb
80
97
  - lib/health_cards/verifier.rb
81
98
  - lib/health_cards/version.rb
82
- homepage: https://github.com/dvci/health-cards
99
+ homepage: https://github.com/dvci/health_cards
83
100
  licenses:
84
101
  - Apache-2.0
85
102
  metadata:
86
- homepage_uri: https://github.com/dvci/health-cards
103
+ homepage_uri: https://github.com/dvci/health_cards
87
104
  source_code_uri: https://github.com/dvci/health_cards
88
- changelog_uri: https://github.com/dvci/health_cards/CHANGELOG.md
105
+ changelog_uri: https://github.com/dvci/health_cards/blob/main/CHANGELOG.md
89
106
  post_install_message:
90
107
  rdoc_options: []
91
108
  require_paths:
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'health_cards/attribute_filters'
4
- require 'health_cards/card_types'
5
-
6
- module HealthCards
7
- # Implements HealthCard for use with COVID Vaccination IG
8
- class COVIDHealthCard < HealthCards::HealthCard
9
- fhir_version '4.0.1'
10
-
11
- additional_types 'https://smarthealth.cards#covid19'
12
- additional_types 'https://smarthealth.cards#immunization'
13
-
14
- allow type: FHIR::Patient, attributes: %w[name birthDate]
15
- allow type: FHIR::Immunization, attributes: %w[status vaccineCode patient occurrenceDateTime]
16
- end
17
- end