fastlane-plugin-rustore_publish 0.3.1

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.
@@ -0,0 +1,523 @@
1
+ require "fastlane/action"
2
+ require_relative "../helper/rustore_auth"
3
+ require_relative "../helper/rustore_client"
4
+ require_relative "../helper/rustore_logger"
5
+
6
+ module Fastlane
7
+ module Actions
8
+ class RustoreUploadAction < Action
9
+ TOTAL_STEPS = 5
10
+
11
+ def self.run(params)
12
+ logger = RustorePublish::RustoreLogger.new(total_steps: TOTAL_STEPS)
13
+
14
+ # ── Step 1: Authentication ─────────────────────────────────────────────
15
+ logger.step(1, "Authentication")
16
+
17
+ private_key = params[:private_key] || File.read(params[:private_key_path])
18
+ auth = RustorePublish::RustoreAuth.new(
19
+ key_id: params[:key_id],
20
+ private_key: private_key,
21
+ logger: logger
22
+ )
23
+ auth.refresh!
24
+ logger.success("JWE token obtained (valid 900s, auto-refresh enabled)")
25
+
26
+ client = RustorePublish::RustoreClient.new(auth: auth, logger: logger, upload_timeout: params[:upload_timeout])
27
+ package_name = params[:package_name]
28
+
29
+ # ── Step 2: Draft Management ───────────────────────────────────────────
30
+ logger.step(2, "Draft Management")
31
+
32
+ existing = client.find_existing_draft(package_name: package_name)
33
+ if existing
34
+ draft_id = existing["publishId"] || existing["versionId"]
35
+ logger.warning("Found existing draft (id=#{draft_id}) — deleting it")
36
+ client.delete_draft(package_name: package_name, version_id: draft_id)
37
+ logger.info("Existing draft deleted")
38
+ end
39
+
40
+ whats_new = params[:whats_new]
41
+ moder_info = params[:moder_info]
42
+ publish_type = params[:publish_type]
43
+ release_date = params[:release_date]
44
+ rollout_percentage = params[:rollout_percentage]
45
+
46
+ if release_date && publish_type != "DELAYED"
47
+ logger.warning("release_date is set but publish_type is '#{publish_type}' — only used with DELAYED")
48
+ end
49
+
50
+ draft_meta = {}
51
+ draft_meta[:whatsNew] = whats_new if whats_new
52
+ draft_meta[:moderInfo] = moder_info if moder_info
53
+ draft_meta[:publishType] = publish_type
54
+ draft_meta[:publishDateTime] = release_date if release_date
55
+ draft_meta[:partialValue] = rollout_percentage if rollout_percentage
56
+
57
+ # Draft metadata (RuStore API v2)
58
+ developer_contacts = params[:developer_contacts]
59
+ draft_meta[:developerContacts] = normalize_developer_contacts(developer_contacts) if developer_contacts
60
+ draft_meta[:appName] = params[:app_name] if params[:app_name]
61
+ draft_meta[:appType] = params[:app_type] if params[:app_type]
62
+ draft_meta[:ageLegal] = params[:age_legal] if params[:age_legal]
63
+ draft_meta[:shortDescription] = params[:short_description] if params[:short_description]
64
+ draft_meta[:fullDescription] = params[:full_description] if params[:full_description]
65
+ draft_meta[:priceValue] = params[:price_value] if params[:price_value]
66
+ draft_meta[:categories] = params[:categories] if params[:categories]
67
+ draft_meta[:seoTagIds] = params[:seo_tag_ids] if params[:seo_tag_ids]
68
+ draft_meta[:faq] = params[:faq] if params[:faq]
69
+
70
+ version_id = client.create_draft(package_name: package_name, **draft_meta)
71
+ logger.success("Draft created (versionId=#{version_id})")
72
+ logger.table([
73
+ ["Package", package_name],
74
+ ["Version ID", version_id],
75
+ ["What's new", whats_new || "(not set)"],
76
+ ["Moderator note", moder_info || "(not set)"],
77
+ ["Publish type", publish_type],
78
+ ["Rollout", rollout_percentage ? "#{rollout_percentage}%" : "100%"],
79
+ ["Release date", release_date || "(not set)"],
80
+ ["Update priority", params[:update_priority]],
81
+ ["Upload timeout", "#{params[:upload_timeout]}s"]
82
+ ])
83
+
84
+ # ── Step 3: Upload primary build ───────────────────────────────────────
85
+ aab_path = params[:aab_path]
86
+ apk_path = params[:apk_path]
87
+ hms_apk_path = params[:hms_apk_path]
88
+
89
+ primary_label = aab_path ? "Uploading AAB (GMS/main)" : "Uploading APK (main)"
90
+ logger.step(3, primary_label)
91
+
92
+ if aab_path
93
+ validate_file!(aab_path, "aab_path", ".aab", logger)
94
+ logger.table([
95
+ ["File", File.basename(aab_path)],
96
+ ["Size", "#{(File.size(aab_path) / 1_048_576.0).round(1)} MB"],
97
+ ["Type", "AAB (main/GMS)"],
98
+ ["services_type", "Unknown"],
99
+ ["is_main", "true (implicit for AAB)"]
100
+ ])
101
+ client.upload_aab(
102
+ package_name: package_name,
103
+ version_id: version_id,
104
+ file_path: aab_path
105
+ )
106
+ logger.success("AAB uploaded successfully")
107
+
108
+ elsif apk_path
109
+ validate_file!(apk_path, "apk_path", ".apk", logger)
110
+ logger.table([
111
+ ["File", File.basename(apk_path)],
112
+ ["Size", "#{(File.size(apk_path) / 1_048_576.0).round(1)} MB"],
113
+ ["Type", "APK (main/GMS)"],
114
+ ["services_type", "Unknown"],
115
+ ["is_main", "true"]
116
+ ])
117
+ client.upload_apk(
118
+ package_name: package_name,
119
+ version_id: version_id,
120
+ file_path: apk_path,
121
+ services_type: RustorePublish::RustoreClient::SERVICES_TYPE_UNKNOWN,
122
+ is_main_apk: true
123
+ )
124
+ logger.success("APK uploaded successfully")
125
+ else
126
+ logger.error(
127
+ "No build file provided",
128
+ hint: "Specify at least `aab_path` or `apk_path`"
129
+ )
130
+ end
131
+
132
+ # ── Step 4: Upload HMS APK (optional) ─────────────────────────────────
133
+ logger.step(4, "Uploading APK (HMS/secondary)")
134
+
135
+ if hms_apk_path
136
+ validate_file!(hms_apk_path, "hms_apk_path", ".apk", logger)
137
+ logger.table([
138
+ ["File", File.basename(hms_apk_path)],
139
+ ["Size", "#{(File.size(hms_apk_path) / 1_048_576.0).round(1)} MB"],
140
+ ["Type", "APK (secondary/HMS)"],
141
+ ["services_type", "HMS"],
142
+ ["is_main", "false"]
143
+ ])
144
+ client.upload_apk(
145
+ package_name: package_name,
146
+ version_id: version_id,
147
+ file_path: hms_apk_path,
148
+ services_type: RustorePublish::RustoreClient::SERVICES_TYPE_HMS,
149
+ is_main_apk: false
150
+ )
151
+ logger.success("HMS APK uploaded successfully")
152
+ else
153
+ logger.info("Skipped (no hms_apk_path provided)")
154
+ end
155
+
156
+ # ── Step 5: Submit for moderation ─────────────────────────────────────
157
+ logger.step(5, "Submitting for moderation")
158
+ client.submit_for_review(package_name: package_name, version_id: version_id, update_priority: params[:update_priority])
159
+
160
+ case publish_type
161
+ when "INSTANTLY"
162
+ logger.success("Submitted — RuStore will publish automatically after moderation")
163
+ when "DELAYED"
164
+ logger.success("Submitted — scheduled for #{release_date}")
165
+ when "MANUAL"
166
+ logger.success("Submitted — publish manually from RuStore Console after moderation passes")
167
+ end
168
+
169
+ logger.finalize
170
+ UI.success("#{RustorePublish::RustoreLogger::PREFIX} All done! #{package_name} versionId=#{version_id} submitted to RuStore.")
171
+ end
172
+
173
+ # ── Parameter definitions ────────────────────────────────────────────────
174
+
175
+ def self.available_options
176
+ [
177
+ # ── Authentication ─────────────────────────────────────────────────
178
+ FastlaneCore::ConfigItem.new(
179
+ key: :key_id,
180
+ env_name: "RUSTORE_KEY_ID",
181
+ description: "API key ID from RuStore Console → Company → API RuStore",
182
+ type: String,
183
+ optional: false
184
+ ),
185
+ FastlaneCore::ConfigItem.new(
186
+ key: :private_key_path,
187
+ env_name: "RUSTORE_PRIVATE_KEY_PATH",
188
+ description: "Path to RSA private key PEM file (mutually exclusive with private_key)",
189
+ type: String,
190
+ optional: true,
191
+ verify_block: proc { |v| UI.user_error!("private_key_path file not found: #{v}") unless File.exist?(v) }
192
+ ),
193
+ FastlaneCore::ConfigItem.new(
194
+ key: :private_key,
195
+ env_name: "RUSTORE_PRIVATE_KEY",
196
+ description: "RSA private key PEM content (mutually exclusive with private_key_path)",
197
+ type: String,
198
+ optional: true,
199
+ sensitive: true
200
+ ),
201
+
202
+ # ── App identity ───────────────────────────────────────────────────
203
+ FastlaneCore::ConfigItem.new(
204
+ key: :package_name,
205
+ env_name: "RUSTORE_PACKAGE_NAME",
206
+ description: "Application package name (e.g. com.example.app)",
207
+ type: String,
208
+ optional: false
209
+ ),
210
+
211
+ # ── Build files ────────────────────────────────────────────────────
212
+ FastlaneCore::ConfigItem.new(
213
+ key: :aab_path,
214
+ env_name: "RUSTORE_AAB_PATH",
215
+ description: "Path to AAB file (Google/GMS build — becomes the main artifact)",
216
+ type: String,
217
+ optional: true,
218
+ verify_block: proc { |v| UI.user_error!("aab_path file not found: #{v}") unless File.exist?(v) }
219
+ ),
220
+ FastlaneCore::ConfigItem.new(
221
+ key: :apk_path,
222
+ env_name: "RUSTORE_APK_PATH",
223
+ description: "Path to APK file (used when aab_path is not provided; becomes main artifact)",
224
+ type: String,
225
+ optional: true,
226
+ verify_block: proc { |v| UI.user_error!("apk_path file not found: #{v}") unless File.exist?(v) }
227
+ ),
228
+ FastlaneCore::ConfigItem.new(
229
+ key: :hms_apk_path,
230
+ env_name: "RUSTORE_HMS_APK_PATH",
231
+ description: "Path to Huawei/HMS APK file (uploaded with servicesType=HMS, isMainApk=false)",
232
+ type: String,
233
+ optional: true,
234
+ verify_block: proc { |v| UI.user_error!("hms_apk_path file not found: #{v}") unless File.exist?(v) }
235
+ ),
236
+
237
+ # ── Release notes ──────────────────────────────────────────────────
238
+ FastlaneCore::ConfigItem.new(
239
+ key: :whats_new,
240
+ env_name: "RUSTORE_WHATS_NEW",
241
+ description: "Release notes shown to users (max 5000 chars, plain text)",
242
+ type: String,
243
+ optional: true,
244
+ verify_block: proc { |v| UI.user_error!("whats_new must not exceed 5000 characters") if v.length > 5000 }
245
+ ),
246
+ FastlaneCore::ConfigItem.new(
247
+ key: :moder_info,
248
+ env_name: "RUSTORE_MODER_INFO",
249
+ description: "Notes for the RuStore moderator, e.g. test credentials (max 180 chars)",
250
+ type: String,
251
+ optional: true,
252
+ verify_block: proc { |v| UI.user_error!("moder_info must not exceed 180 characters") if v.length > 180 }
253
+ ),
254
+
255
+ # ── Publication ────────────────────────────────────────────────────
256
+ FastlaneCore::ConfigItem.new(
257
+ key: :publish_type,
258
+ env_name: "RUSTORE_PUBLISH_TYPE",
259
+ description: "INSTANTLY — auto-publish after moderation; MANUAL — publish from Console; DELAYED — scheduled via release_date",
260
+ default_value: "INSTANTLY",
261
+ type: String,
262
+ optional: true,
263
+ verify_block: proc { |v| UI.user_error!("publish_type must be INSTANTLY, MANUAL, or DELAYED") unless %w[INSTANTLY MANUAL DELAYED].include?(v) }
264
+ ),
265
+ FastlaneCore::ConfigItem.new(
266
+ key: :release_date,
267
+ env_name: "RUSTORE_RELEASE_DATE",
268
+ description: "Scheduled release datetime in ISO 8601 format (only used with DELAYED)",
269
+ type: String,
270
+ optional: true
271
+ ),
272
+ FastlaneCore::ConfigItem.new(
273
+ key: :rollout_percentage,
274
+ env_name: "RUSTORE_ROLLOUT_PERCENTAGE",
275
+ description: "Staged rollout: allowed values are 5, 10, 25, 50, 75, 100; omit for full rollout",
276
+ type: Integer,
277
+ optional: true,
278
+ verify_block: proc { |v|
279
+ UI.user_error!("rollout_percentage must be one of: 5, 10, 25, 50, 75, 100") unless [5, 10, 25, 50, 75, 100].include?(v)
280
+ }
281
+ ),
282
+ FastlaneCore::ConfigItem.new(
283
+ key: :update_priority,
284
+ env_name: "RUSTORE_UPDATE_PRIORITY",
285
+ description: "In-app update priority sent with the moderation request: 0 (min) to 5 (max)",
286
+ default_value: 0,
287
+ type: Integer,
288
+ optional: true,
289
+ verify_block: proc { |v| UI.user_error!("update_priority must be between 0 and 5") unless (0..5).cover?(v) }
290
+ ),
291
+
292
+ # ── Network ────────────────────────────────────────────────────────
293
+ FastlaneCore::ConfigItem.new(
294
+ key: :upload_timeout,
295
+ env_name: "RUSTORE_UPLOAD_TIMEOUT",
296
+ description: "Seconds to wait for RuStore to answer an AAB/APK upload; RuStore checks the file before answering",
297
+ default_value: RustorePublish::RustoreClient::DEFAULT_UPLOAD_TIMEOUT,
298
+ type: Integer,
299
+ optional: true,
300
+ verify_block: proc { |v| UI.user_error!("upload_timeout must be a positive number of seconds") unless v > 0 }
301
+ ),
302
+
303
+ # ── Draft metadata (RuStore API v2) ──────────────────────────────
304
+ FastlaneCore::ConfigItem.new(
305
+ key: :developer_contacts,
306
+ description: "Developer contact info Hash: { email:, website:, vk_community: } — at least one key required",
307
+ type: Hash,
308
+ optional: true,
309
+ verify_block: proc { |v|
310
+ UI.user_error!("developer_contacts must contain at least one of: email, website, vk_community") if v.empty?
311
+ allowed = %i[email website vk_community]
312
+ v.each_key do |k|
313
+ unless allowed.include?(k.to_sym)
314
+ UI.user_error!("developer_contacts: unknown sub-key '#{k}' (accepted: #{allowed.join(', ')})")
315
+ end
316
+ end
317
+ }
318
+ ),
319
+ FastlaneCore::ConfigItem.new(
320
+ key: :app_name,
321
+ env_name: "RUSTORE_APP_NAME",
322
+ description: "App display name (max 50 chars)",
323
+ type: String,
324
+ optional: true,
325
+ verify_block: proc { |v|
326
+ UI.user_error!("app_name must not exceed 50 characters") if v.length > 50
327
+ }
328
+ ),
329
+ FastlaneCore::ConfigItem.new(
330
+ key: :app_type,
331
+ env_name: "RUSTORE_APP_TYPE",
332
+ description: "App type: GAMES or MAIN",
333
+ type: String,
334
+ optional: true,
335
+ verify_block: proc { |v|
336
+ UI.user_error!("app_type must be 'GAMES' or 'MAIN'") unless %w[GAMES MAIN].include?(v)
337
+ }
338
+ ),
339
+ FastlaneCore::ConfigItem.new(
340
+ key: :age_legal,
341
+ env_name: "RUSTORE_AGE_LEGAL",
342
+ description: "Age rating: 0+, 6+, 12+, 16+, or 18+",
343
+ type: String,
344
+ optional: true,
345
+ verify_block: proc { |v|
346
+ allowed = %w[0+ 6+ 12+ 16+ 18+]
347
+ UI.user_error!("age_legal must be one of: #{allowed.join(', ')}") unless allowed.include?(v)
348
+ }
349
+ ),
350
+ FastlaneCore::ConfigItem.new(
351
+ key: :short_description,
352
+ env_name: "RUSTORE_SHORT_DESCRIPTION",
353
+ description: "Brief description shown in app list (max 80 chars)",
354
+ type: String,
355
+ optional: true,
356
+ verify_block: proc { |v|
357
+ UI.user_error!("short_description must not exceed 80 characters") if v.length > 80
358
+ }
359
+ ),
360
+ FastlaneCore::ConfigItem.new(
361
+ key: :full_description,
362
+ env_name: "RUSTORE_FULL_DESCRIPTION",
363
+ description: "Full description on the app page (max 4000 chars)",
364
+ type: String,
365
+ optional: true,
366
+ verify_block: proc { |v|
367
+ UI.user_error!("full_description must not exceed 4000 characters") if v.length > 4000
368
+ }
369
+ ),
370
+ FastlaneCore::ConfigItem.new(
371
+ key: :price_value,
372
+ env_name: "RUSTORE_PRICE_VALUE",
373
+ description: "Price in kopecks as a string of digits, e.g. '8799' for 87.99 RUB",
374
+ type: String,
375
+ optional: true,
376
+ verify_block: proc { |v|
377
+ UI.user_error!("price_value must be a non-empty string of digits") unless v.is_a?(String) && v =~ /\A\d+\z/
378
+ }
379
+ ),
380
+ FastlaneCore::ConfigItem.new(
381
+ key: :categories,
382
+ description: "App categories, e.g. ['news', 'education'] — max 2 elements",
383
+ type: Array,
384
+ optional: true,
385
+ verify_block: proc { |v|
386
+ UI.user_error!("categories must be an Array") unless v.is_a?(Array)
387
+ UI.user_error!("categories must not exceed 2 elements") if v.size > 2
388
+ v.each do |c|
389
+ unless c.is_a?(String) && !c.strip.empty?
390
+ UI.user_error!("categories elements must be non-empty strings")
391
+ end
392
+ end
393
+ }
394
+ ),
395
+ FastlaneCore::ConfigItem.new(
396
+ key: :seo_tag_ids,
397
+ description: "RuStore search tag IDs (max 5 integers)",
398
+ type: Array,
399
+ optional: true,
400
+ verify_block: proc { |v|
401
+ UI.user_error!("seo_tag_ids must be an Array") unless v.is_a?(Array)
402
+ UI.user_error!("seo_tag_ids must not exceed 5 elements") if v.size > 5
403
+ v.each do |t|
404
+ UI.user_error!("seo_tag_ids elements must be integers") unless t.is_a?(Integer)
405
+ end
406
+ }
407
+ ),
408
+ FastlaneCore::ConfigItem.new(
409
+ key: :faq,
410
+ description: "FAQ items array (max 10), each Hash with question (max 120 chars) and answer (max 500 chars)",
411
+ type: Array,
412
+ optional: true,
413
+ verify_block: proc { |v|
414
+ UI.user_error!("faq must be an Array") unless v.is_a?(Array)
415
+ UI.user_error!("faq must not exceed 10 elements") if v.size > 10
416
+ v.each_with_index do |item, idx|
417
+ UI.user_error!("faq[#{idx}] must be a Hash") unless item.is_a?(Hash)
418
+ q = item[:question] || item["question"]
419
+ a = item[:answer] || item["answer"]
420
+ UI.user_error!("faq[#{idx}] missing :question") if q.nil?
421
+ UI.user_error!("faq[#{idx}] missing :answer") if a.nil?
422
+ UI.user_error!("faq[#{idx}].question must not exceed 120 characters") if q.length > 120
423
+ UI.user_error!("faq[#{idx}].answer must not exceed 500 characters") if a.length > 500
424
+ end
425
+ }
426
+ )
427
+ ]
428
+ end
429
+
430
+ def self.description
431
+ "Upload and publish Android apps (APK/AAB) to RuStore app store"
432
+ end
433
+
434
+ def self.details
435
+ <<~DETAILS
436
+ Uploads an Android application to the RuStore app store via the RuStore Public API.
437
+
438
+ Supports multi-file versions in a single API version:
439
+ - AAB (Google/GMS build) as the main artifact
440
+ - APK (Huawei/HMS build) as a secondary artifact (servicesType=HMS)
441
+
442
+ The pipeline always finishes immediately after submitting for moderation.
443
+ Publication behaviour is controlled by publish_type:
444
+ INSTANTLY — RuStore auto-publishes after moderation passes (default)
445
+ DELAYED — scheduled publication via release_date
446
+ MANUAL — moderation runs, then publish manually from RuStore Console
447
+
448
+ GitLab CI: when GITLAB_CI=true, each step is wrapped in a collapsible
449
+ section visible in the Pipeline web UI.
450
+
451
+ Requires at least one active version already published in RuStore Console.
452
+ DETAILS
453
+ end
454
+
455
+ def self.authors
456
+ ["Sergey Gorban"]
457
+ end
458
+
459
+ def self.example_usage
460
+ [
461
+ '# Minimal — AAB only, INSTANTLY (default)',
462
+ 'rustore_upload(',
463
+ ' package_name: "com.example.app",',
464
+ ' key_id: ENV["RUSTORE_KEY_ID"],',
465
+ ' private_key_path: "rustore_private_key.pem",',
466
+ ' aab_path: "app/build/outputs/bundle/release/app-release.aab"',
467
+ ')',
468
+ '',
469
+ '# AAB (GMS) + APK (HMS), staged rollout',
470
+ 'rustore_upload(',
471
+ ' package_name: "com.example.app",',
472
+ ' key_id: ENV["RUSTORE_KEY_ID"],',
473
+ ' private_key: ENV["RUSTORE_PRIVATE_KEY"],',
474
+ ' aab_path: "app/build/outputs/bundle/gmsRelease/app-gms-release.aab",',
475
+ ' hms_apk_path: "app/build/outputs/apk/hmsRelease/app-hms-release.apk",',
476
+ ' publish_type: "INSTANTLY",',
477
+ ' rollout_percentage: 20',
478
+ ')',
479
+ '',
480
+ '# Scheduled release',
481
+ 'rustore_upload(',
482
+ ' package_name: "com.example.app",',
483
+ ' key_id: ENV["RUSTORE_KEY_ID"],',
484
+ ' private_key: ENV["RUSTORE_PRIVATE_KEY"],',
485
+ ' aab_path: "...",',
486
+ ' publish_type: "DELAYED",',
487
+ ' release_date: "2025-03-01T10:00:00+03:00"',
488
+ ')'
489
+ ].join("\n")
490
+ end
491
+
492
+ def self.is_supported?(platform)
493
+ platform == :android
494
+ end
495
+
496
+ private
497
+
498
+ def self.validate_file!(path, param_name, expected_ext, logger)
499
+ unless File.exist?(path)
500
+ logger.error(
501
+ "File not found for #{param_name}: #{path}",
502
+ hint: "Check that the build step ran before rustore_upload and the path is correct"
503
+ )
504
+ end
505
+ ext = File.extname(path).downcase
506
+ unless ext == expected_ext
507
+ logger.error(
508
+ "#{param_name} has unexpected extension '#{ext}' (expected '#{expected_ext}'): #{path}",
509
+ hint: "Make sure you passed the correct file path"
510
+ )
511
+ end
512
+ end
513
+
514
+ def self.normalize_developer_contacts(contacts)
515
+ map = { email: :email, website: :website, vk_community: :vkCommunity }
516
+ contacts.each_with_object({}) do |(k, v), out|
517
+ mapped = map[k.to_sym]
518
+ out[mapped] = v if mapped
519
+ end
520
+ end
521
+ end
522
+ end
523
+ end
@@ -0,0 +1,33 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
3
+ PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
4
+ ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
5
+ Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS
6
+ VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
7
+ YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v
8
+ dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n
9
+ qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q
10
+ XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U
11
+ zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX
12
+ YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y
13
+ Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD
14
+ U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD
15
+ 4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9
16
+ G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH
17
+ BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX
18
+ ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa
19
+ OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf
20
+ BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS
21
+ BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
22
+ AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH
23
+ tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq
24
+ W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+
25
+ /3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS
26
+ AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj
27
+ C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV
28
+ 4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d
29
+ WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ
30
+ D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC
31
+ EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq
32
+ 391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4=
33
+ -----END CERTIFICATE-----