health_cards 0.0.2 → 1.0.0

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 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