@solana-mobile/dapp-store-cli 0.16.0 → 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.
Files changed (113) hide show
  1. package/bin/dapp-store.js +3 -1
  2. package/lib/CliSetup.js +304 -505
  3. package/lib/CliUtils.js +6 -376
  4. package/lib/__tests__/CliSetupTest.js +484 -74
  5. package/lib/cli/__tests__/parseErrors.test.js +25 -0
  6. package/lib/cli/__tests__/signer.test.js +436 -0
  7. package/lib/cli/constants.js +23 -0
  8. package/lib/cli/messages.js +21 -0
  9. package/lib/cli/parseErrors.js +41 -0
  10. package/lib/{commands/publish/PublishCliSupport.js → cli/selfUpdate.js} +72 -38
  11. package/lib/{commands/publish/PublishCliRemove.js → cli/signer.js} +35 -56
  12. package/lib/index.js +96 -5
  13. package/lib/package.json +5 -24
  14. package/lib/portal/__tests__/releaseMetadata.test.js +647 -0
  15. package/lib/portal/__tests__/translators.test.js +76 -0
  16. package/lib/portal/__tests__/workflowClient.test.js +457 -0
  17. package/lib/portal/attestationClient.js +143 -0
  18. package/lib/portal/files.js +64 -0
  19. package/lib/portal/http.js +364 -0
  20. package/lib/portal/records.js +64 -0
  21. package/lib/portal/releaseMetadata.js +748 -0
  22. package/lib/portal/translators.js +460 -0
  23. package/lib/portal/types.js +1 -0
  24. package/lib/portal/workflowClient.js +704 -0
  25. package/lib/publication/PublicationProgressReporter.js +1051 -0
  26. package/lib/publication/__tests__/PublicationProgressReporter.test.js +174 -0
  27. package/lib/{commands/ValidateCommand.js → publication/__tests__/fundingPreflight.test.js} +90 -66
  28. package/lib/publication/__tests__/publicationSummary.test.js +26 -0
  29. package/lib/publication/cliValidation.js +482 -0
  30. package/lib/publication/fundingPreflight.js +246 -0
  31. package/lib/publication/publicationSummary.js +99 -0
  32. package/lib/{commands/utils.js → publication/runPublicationWorkflow.js} +16 -46
  33. package/package.json +5 -24
  34. package/src/CliSetup.ts +370 -505
  35. package/src/CliUtils.ts +9 -233
  36. package/src/__tests__/CliSetupTest.ts +272 -120
  37. package/src/cli/__tests__/parseErrors.test.ts +34 -0
  38. package/src/cli/__tests__/signer.test.ts +359 -0
  39. package/src/cli/constants.ts +3 -0
  40. package/src/cli/messages.ts +27 -0
  41. package/src/cli/parseErrors.ts +62 -0
  42. package/src/cli/selfUpdate.ts +59 -0
  43. package/src/cli/signer.ts +38 -0
  44. package/src/index.ts +31 -4
  45. package/src/portal/__tests__/releaseMetadata.test.ts +508 -0
  46. package/src/portal/__tests__/translators.test.ts +82 -0
  47. package/src/portal/__tests__/workflowClient.test.ts +278 -0
  48. package/src/portal/attestationClient.ts +19 -0
  49. package/src/portal/files.ts +73 -0
  50. package/src/portal/http.ts +170 -0
  51. package/src/portal/records.ts +38 -0
  52. package/src/portal/releaseMetadata.ts +489 -0
  53. package/src/portal/translators.ts +750 -0
  54. package/src/portal/types.ts +27 -0
  55. package/src/portal/workflowClient.ts +575 -0
  56. package/src/publication/PublicationProgressReporter.ts +1026 -0
  57. package/src/publication/__tests__/PublicationProgressReporter.test.ts +210 -0
  58. package/src/publication/__tests__/fundingPreflight.test.ts +78 -0
  59. package/src/publication/__tests__/publicationSummary.test.ts +30 -0
  60. package/src/publication/cliValidation.ts +264 -0
  61. package/src/publication/fundingPreflight.ts +123 -0
  62. package/src/publication/publicationSummary.ts +26 -0
  63. package/src/publication/runPublicationWorkflow.ts +46 -0
  64. package/lib/commands/create/CreateCliApp.js +0 -223
  65. package/lib/commands/create/CreateCliRelease.js +0 -290
  66. package/lib/commands/create/index.js +0 -40
  67. package/lib/commands/index.js +0 -3
  68. package/lib/commands/publish/PublishCliSubmit.js +0 -208
  69. package/lib/commands/publish/PublishCliUpdate.js +0 -211
  70. package/lib/commands/publish/index.js +0 -22
  71. package/lib/commands/scaffolding/ScaffoldInit.js +0 -15
  72. package/lib/commands/scaffolding/index.js +0 -1
  73. package/lib/config/EnvVariables.js +0 -59
  74. package/lib/config/PublishDetails.js +0 -915
  75. package/lib/config/S3StorageManager.js +0 -93
  76. package/lib/config/index.js +0 -2
  77. package/lib/generated/config_obj.json +0 -1
  78. package/lib/generated/config_schema.json +0 -1
  79. package/lib/prebuild_schema/publishing_source.yaml +0 -64
  80. package/lib/prebuild_schema/schemagen.js +0 -25
  81. package/lib/upload/CachedStorageDriver.js +0 -458
  82. package/lib/upload/TurboStorageDriver.js +0 -718
  83. package/lib/upload/__tests__/CachedStorageDriver.test.js +0 -437
  84. package/lib/upload/__tests__/TurboStorageDriver.test.js +0 -17
  85. package/lib/upload/__tests__/contentGateway.test.js +0 -17
  86. package/lib/upload/contentGateway.js +0 -23
  87. package/lib/upload/index.js +0 -2
  88. package/src/commands/ValidateCommand.ts +0 -82
  89. package/src/commands/create/CreateCliApp.ts +0 -93
  90. package/src/commands/create/CreateCliRelease.ts +0 -149
  91. package/src/commands/create/index.ts +0 -47
  92. package/src/commands/index.ts +0 -3
  93. package/src/commands/publish/PublishCliRemove.ts +0 -66
  94. package/src/commands/publish/PublishCliSubmit.ts +0 -93
  95. package/src/commands/publish/PublishCliSupport.ts +0 -66
  96. package/src/commands/publish/PublishCliUpdate.ts +0 -101
  97. package/src/commands/publish/index.ts +0 -29
  98. package/src/commands/scaffolding/ScaffoldInit.ts +0 -20
  99. package/src/commands/scaffolding/index.ts +0 -1
  100. package/src/commands/utils.ts +0 -33
  101. package/src/config/EnvVariables.ts +0 -39
  102. package/src/config/PublishDetails.ts +0 -456
  103. package/src/config/S3StorageManager.ts +0 -47
  104. package/src/config/index.ts +0 -2
  105. package/src/prebuild_schema/publishing_source.yaml +0 -64
  106. package/src/prebuild_schema/schemagen.js +0 -31
  107. package/src/upload/CachedStorageDriver.ts +0 -179
  108. package/src/upload/TurboStorageDriver.ts +0 -283
  109. package/src/upload/__tests__/CachedStorageDriver.test.ts +0 -246
  110. package/src/upload/__tests__/TurboStorageDriver.test.ts +0 -15
  111. package/src/upload/__tests__/contentGateway.test.ts +0 -31
  112. package/src/upload/contentGateway.ts +0 -37
  113. package/src/upload/index.ts +0 -2
@@ -0,0 +1,489 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import type {
7
+ PublicationBundle,
8
+ PublicationCreateUploadTargetInput,
9
+ PublicationCreateUploadTargetResult,
10
+ } from "@solana-mobile/dapp-store-publishing-tools";
11
+ import getVideoDimensions from "get-video-dimensions";
12
+ import { imageSize } from "image-size";
13
+
14
+ import {
15
+ ensureHttpsUrl,
16
+ inferFileNameFromUrl,
17
+ inferMimeType,
18
+ } from "./files.js";
19
+ import { uploadBytes } from "./http.js";
20
+ import { firstString, readDeep } from "./records.js";
21
+ import type { PortalSourceKind } from "./types.js";
22
+
23
+ const NFT_SCHEMA_VERSION = "0.4.0";
24
+ const DEFAULT_R2_PUBLIC_HOSTS = [
25
+ "r2.solanamobiledappstore.com",
26
+ "r2-staging.solanamobiledappstore.com",
27
+ ];
28
+
29
+ type RemoteFilePayload = {
30
+ data: string;
31
+ fileName: string;
32
+ mimeType: string;
33
+ };
34
+
35
+ export type ReleaseMetadataPortalClient = {
36
+ createUploadTarget(
37
+ input: PublicationCreateUploadTargetInput
38
+ ): Promise<PublicationCreateUploadTargetResult>;
39
+ fetchRemoteFile(input: {
40
+ url: string;
41
+ fileName?: string;
42
+ expectedMimeType?: string;
43
+ }): Promise<RemoteFilePayload>;
44
+ };
45
+
46
+ type PublicationMediaPurpose =
47
+ | "icon"
48
+ | "screenshot"
49
+ | "banner"
50
+ | "featureGraphic";
51
+
52
+ function asStringArray(value: unknown): string[] {
53
+ return Array.isArray(value)
54
+ ? value.filter((item): item is string => typeof item === "string")
55
+ : [];
56
+ }
57
+
58
+ function normalizeMimeType(value?: string | null): string | undefined {
59
+ return value?.split(";")[0]?.trim().toLowerCase();
60
+ }
61
+
62
+ function toPositiveNumber(value: unknown): number | undefined {
63
+ return typeof value === "number" && Number.isFinite(value) && value > 0
64
+ ? value
65
+ : undefined;
66
+ }
67
+
68
+ function isR2PublicUrl(url: string): boolean {
69
+ try {
70
+ const hostname = new URL(url).hostname.toLowerCase();
71
+ return DEFAULT_R2_PUBLIC_HOSTS.includes(hostname);
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ function inferExtensionFromMimeType(mimeType: string): string | undefined {
78
+ switch (mimeType) {
79
+ case "image/png":
80
+ return "png";
81
+ case "image/jpeg":
82
+ return "jpg";
83
+ case "image/webp":
84
+ return "webp";
85
+ case "image/gif":
86
+ return "gif";
87
+ case "image/svg+xml":
88
+ return "svg";
89
+ case "video/mp4":
90
+ return "mp4";
91
+ case "video/webm":
92
+ return "webm";
93
+ case "video/quicktime":
94
+ return "mov";
95
+ case "application/json":
96
+ return "json";
97
+ default: {
98
+ const subtype = mimeType
99
+ .split("/")[1]
100
+ ?.split("+")[0]
101
+ ?.trim()
102
+ .toLowerCase();
103
+ return subtype?.replace(/[^a-z0-9]/g, "") || undefined;
104
+ }
105
+ }
106
+ }
107
+
108
+ function inferUploadFileExtension(fileName: string, mimeType: string): string {
109
+ const byName = fileName.split(".").pop()?.trim().toLowerCase();
110
+ if (byName && /^[a-z0-9]+$/.test(byName)) {
111
+ return byName;
112
+ }
113
+
114
+ return inferExtensionFromMimeType(mimeType) || "bin";
115
+ }
116
+
117
+ function normalizeOptionalUrl(value: string | null | undefined) {
118
+ if (!value) {
119
+ return undefined;
120
+ }
121
+
122
+ const trimmed = value.trim();
123
+ if (!trimmed) {
124
+ return undefined;
125
+ }
126
+
127
+ return ensureHttpsUrl(trimmed);
128
+ }
129
+
130
+ function normalizeReleaseName(value: string): string {
131
+ const trimmed = value.trim();
132
+ return trimmed.length <= 32 ? trimmed : trimmed.slice(0, 32);
133
+ }
134
+
135
+ function isVideoMimeType(mimeType: string): boolean {
136
+ return mimeType.startsWith("video/");
137
+ }
138
+
139
+ async function getVideoMediaDimensions(
140
+ fileBytes: Uint8Array,
141
+ fileName: string,
142
+ mimeType: string
143
+ ): Promise<{ width: number; height: number }> {
144
+ const tempFilePath = path.join(
145
+ os.tmpdir(),
146
+ `dapp-store-media-${process.pid}-${Date.now()}-${Math.random()
147
+ .toString(36)
148
+ .slice(2)}.${inferUploadFileExtension(fileName, mimeType)}`
149
+ );
150
+
151
+ await fs.writeFile(tempFilePath, fileBytes);
152
+
153
+ try {
154
+ const dimensions = await getVideoDimensions(tempFilePath);
155
+ const width = toPositiveNumber(dimensions.width);
156
+ const height = toPositiveNumber(dimensions.height);
157
+
158
+ if (!width || !height) {
159
+ throw new Error(`Unable to determine video dimensions for ${fileName}`);
160
+ }
161
+
162
+ return { width, height };
163
+ } finally {
164
+ await fs.rm(tempFilePath, { force: true });
165
+ }
166
+ }
167
+
168
+ async function getMediaDimensions(
169
+ fileBytes: Uint8Array,
170
+ fileName: string,
171
+ mimeType: string
172
+ ): Promise<{ width: number; height: number }> {
173
+ if (isVideoMimeType(mimeType)) {
174
+ return await getVideoMediaDimensions(fileBytes, fileName, mimeType);
175
+ }
176
+
177
+ const dimensions = imageSize(fileBytes);
178
+ const width = toPositiveNumber(dimensions.width);
179
+ const height = toPositiveNumber(dimensions.height);
180
+
181
+ if (!width || !height) {
182
+ throw new Error(`Unable to determine image dimensions for ${fileName}`);
183
+ }
184
+
185
+ return { width, height };
186
+ }
187
+
188
+ function getPreferredFeatureGraphicUrl(bundle: PublicationBundle): string {
189
+ return (
190
+ firstString(bundle, [
191
+ "dapp.editorsChoiceGraphicUrl",
192
+ "dapp.featureGraphicUrl",
193
+ ]) || ""
194
+ );
195
+ }
196
+
197
+ async function resolveMediaItem(
198
+ client: ReleaseMetadataPortalClient,
199
+ input: {
200
+ defaultMimeType?: string;
201
+ expectedMimeType?: string;
202
+ fallbackFileName: string;
203
+ purpose: PublicationMediaPurpose;
204
+ uri: string;
205
+ }
206
+ ) {
207
+ const resolvedUri = ensureHttpsUrl(input.uri);
208
+ const isAlreadyPublicR2 = isR2PublicUrl(resolvedUri);
209
+
210
+ let remoteFile: RemoteFilePayload;
211
+ try {
212
+ remoteFile = await client.fetchRemoteFile({
213
+ url: resolvedUri,
214
+ fileName: input.fallbackFileName,
215
+ expectedMimeType: input.expectedMimeType,
216
+ });
217
+ } catch (error) {
218
+ const message = error instanceof Error ? error.message : String(error);
219
+ throw new Error(
220
+ `Failed to fetch ${input.purpose} media from ${resolvedUri}: ${message}`
221
+ );
222
+ }
223
+ const fileBytes = Buffer.from(remoteFile.data, "base64");
224
+
225
+ if (fileBytes.byteLength === 0) {
226
+ throw new Error(`Remote media file is empty: ${resolvedUri}`);
227
+ }
228
+
229
+ const remoteFileName = remoteFile.fileName || input.fallbackFileName;
230
+ const resolvedMimeType =
231
+ normalizeMimeType(remoteFile.mimeType) ||
232
+ input.defaultMimeType ||
233
+ inferMimeType(remoteFileName);
234
+ const fileHash = createHash("sha256").update(fileBytes).digest("hex");
235
+ const dimensions = await getMediaDimensions(
236
+ fileBytes,
237
+ remoteFileName,
238
+ resolvedMimeType
239
+ );
240
+
241
+ if (isAlreadyPublicR2) {
242
+ return {
243
+ mime: resolvedMimeType,
244
+ purpose: input.purpose,
245
+ uri: resolvedUri,
246
+ width: dimensions.width,
247
+ height: dimensions.height,
248
+ sha256: fileHash,
249
+ };
250
+ }
251
+
252
+ const uploadTarget = await client.createUploadTarget({
253
+ fileHash,
254
+ fileExtension: inferUploadFileExtension(remoteFileName, resolvedMimeType),
255
+ contentType: resolvedMimeType,
256
+ });
257
+
258
+ if (!uploadTarget.uploadUrl || !uploadTarget.publicUrl) {
259
+ throw new Error(
260
+ `The portal did not return a valid upload target for ${input.purpose}.`
261
+ );
262
+ }
263
+
264
+ await uploadBytes(uploadTarget.uploadUrl, fileBytes, resolvedMimeType);
265
+
266
+ return {
267
+ mime: resolvedMimeType,
268
+ purpose: input.purpose,
269
+ uri: uploadTarget.publicUrl,
270
+ width: dimensions.width,
271
+ height: dimensions.height,
272
+ sha256: fileHash,
273
+ };
274
+ }
275
+
276
+ export async function buildReleaseMetadataDocument(
277
+ client: ReleaseMetadataPortalClient,
278
+ bundle: PublicationBundle,
279
+ sourceKind: PortalSourceKind
280
+ ): Promise<Record<string, unknown>> {
281
+ const releaseName = normalizeReleaseName(
282
+ bundle.metadata?.localizedName ||
283
+ bundle.release.localizedName ||
284
+ bundle.release.releaseName ||
285
+ bundle.dapp.dappName ||
286
+ "Release update"
287
+ );
288
+ const shortDescription =
289
+ bundle.metadata?.shortDescription ||
290
+ bundle.release.shortDescription ||
291
+ bundle.dapp.subtitle ||
292
+ "Release NFT";
293
+ const longDescription =
294
+ bundle.metadata?.longDescription ||
295
+ bundle.release.longDescription ||
296
+ bundle.dapp.description ||
297
+ shortDescription;
298
+ const newInVersion =
299
+ bundle.metadata?.newInVersion || bundle.release.newInVersion || "";
300
+ const publisherAddress =
301
+ bundle.signerAuthority.dappWalletAddress ||
302
+ bundle.signerAuthority.collectionAuthority ||
303
+ bundle.dapp.walletAddress ||
304
+ "";
305
+ const publisherName = bundle.publisher.name || "";
306
+ const publisherWebsite = normalizeOptionalUrl(
307
+ bundle.metadata?.publisherWebsite ||
308
+ bundle.publisher.website ||
309
+ bundle.dapp.appWebsite ||
310
+ bundle.dapp.website ||
311
+ undefined
312
+ );
313
+ const publisherContact =
314
+ bundle.publisher.email ||
315
+ bundle.dapp.contactEmail ||
316
+ bundle.dapp.supportEmail ||
317
+ "";
318
+ const publisherSupportEmail =
319
+ bundle.publisher.supportEmail ||
320
+ bundle.metadata?.supportEmail ||
321
+ bundle.dapp.supportEmail ||
322
+ bundle.dapp.contactEmail ||
323
+ bundle.publisher.email ||
324
+ publisherContact;
325
+ const iconUri = bundle.dapp.dappIconUrl || "";
326
+
327
+ if (!iconUri) {
328
+ throw new Error(
329
+ "Publication bundle did not include a public app icon URL."
330
+ );
331
+ }
332
+
333
+ const installUri =
334
+ bundle.installFile.uri || bundle.release.releaseFileUrl || "";
335
+ const installMimeType =
336
+ bundle.installFile.mimeType ||
337
+ inferMimeType(inferFileNameFromUrl(installUri));
338
+ const installSize =
339
+ bundle.installFile.size ?? bundle.release.releaseFileSize ?? 0;
340
+ const installSha256 =
341
+ bundle.installFile.sha256 || bundle.release.releaseFileHash || "";
342
+ const androidPackage =
343
+ bundle.release.androidPackage || bundle.dapp.androidPackage;
344
+ const versionName = bundle.release.versionName || "";
345
+ const versionCode = bundle.release.versionCode ?? 0;
346
+ const minSdkVersion = bundle.release.minSdkVersion ?? 1;
347
+ const targetSdkRaw = readDeep(bundle, "release.targetSdkVersion");
348
+ const targetSdkVersion =
349
+ typeof targetSdkRaw === "number"
350
+ ? targetSdkRaw
351
+ : Number(targetSdkRaw || 0) || null;
352
+ const certificateFingerprint = bundle.release.certificateFingerprint || "";
353
+ const permissions = bundle.release.permissions ?? [];
354
+ const locales =
355
+ bundle.release.locales && bundle.release.locales.length > 0
356
+ ? bundle.release.locales
357
+ : bundle.metadata?.locales && bundle.metadata.locales.length > 0
358
+ ? bundle.metadata.locales
359
+ : bundle.dapp.languages && bundle.dapp.languages.length > 0
360
+ ? bundle.dapp.languages
361
+ : ["en-US"];
362
+ const previewUris = bundle.dapp.dappPreviewUrls ?? [];
363
+ const bannerUri = bundle.dapp.bannerUrl || "";
364
+ const featureGraphicUri = getPreferredFeatureGraphicUrl(bundle);
365
+
366
+ const media: Array<Record<string, unknown>> = [];
367
+ const iconMedia = await resolveMediaItem(client, {
368
+ defaultMimeType: "image/png",
369
+ fallbackFileName: "release-icon.png",
370
+ purpose: "icon",
371
+ uri: iconUri,
372
+ });
373
+ media.push(iconMedia);
374
+
375
+ for (const [index, previewUri] of previewUris.entries()) {
376
+ media.push(
377
+ await resolveMediaItem(client, {
378
+ fallbackFileName: `release-screenshot-${index + 1}`,
379
+ purpose: "screenshot",
380
+ uri: previewUri,
381
+ })
382
+ );
383
+ }
384
+
385
+ if (bannerUri) {
386
+ media.push(
387
+ await resolveMediaItem(client, {
388
+ defaultMimeType: inferMimeType(inferFileNameFromUrl(bannerUri)),
389
+ fallbackFileName: "release-banner.png",
390
+ purpose: "banner",
391
+ uri: bannerUri,
392
+ })
393
+ );
394
+ }
395
+
396
+ if (featureGraphicUri) {
397
+ media.push(
398
+ await resolveMediaItem(client, {
399
+ defaultMimeType: inferMimeType(
400
+ inferFileNameFromUrl(featureGraphicUri)
401
+ ),
402
+ fallbackFileName: "release-feature-graphic.png",
403
+ purpose: "featureGraphic",
404
+ uri: featureGraphicUri,
405
+ })
406
+ );
407
+ }
408
+
409
+ const licenseUrl = normalizeOptionalUrl(
410
+ bundle.metadata?.legal.licenseUrl || bundle.dapp.licenseUrl || undefined
411
+ );
412
+ const copyrightUrl =
413
+ bundle.metadata?.legal.copyrightUrl ||
414
+ bundle.dapp.copyrightUrl ||
415
+ undefined;
416
+ const privacyPolicyUrl = normalizeOptionalUrl(
417
+ bundle.metadata?.legal.privacyPolicyUrl ||
418
+ bundle.dapp.privacyPolicyUrl ||
419
+ undefined
420
+ );
421
+
422
+ return {
423
+ schema_version: NFT_SCHEMA_VERSION,
424
+ name: releaseName,
425
+ description: shortDescription,
426
+ image: iconMedia.uri,
427
+ ...(publisherWebsite ? { external_url: publisherWebsite } : {}),
428
+ properties: {
429
+ category: "dApp",
430
+ creators: [
431
+ {
432
+ address: publisherAddress,
433
+ share: 100,
434
+ },
435
+ ],
436
+ },
437
+ extensions: {
438
+ solana_dapp_store: {
439
+ publisher_details: {
440
+ name: publisherName,
441
+ ...(publisherWebsite ? { website: publisherWebsite } : {}),
442
+ contact: publisherContact,
443
+ support_email: publisherSupportEmail,
444
+ },
445
+ release_details: {
446
+ updated_on: new Date().toISOString(),
447
+ ...(licenseUrl ? { license_url: licenseUrl } : {}),
448
+ ...(copyrightUrl ? { copyright_url: copyrightUrl } : {}),
449
+ ...(privacyPolicyUrl ? { privacy_policy_url: privacyPolicyUrl } : {}),
450
+ localized_resources: {
451
+ long_description: "1",
452
+ new_in_version: "2",
453
+ name: "4",
454
+ short_description: "5",
455
+ },
456
+ },
457
+ media,
458
+ files: [
459
+ {
460
+ mime: installMimeType,
461
+ purpose: "install",
462
+ size: installSize,
463
+ sha256: installSha256,
464
+ uri: installUri,
465
+ },
466
+ ],
467
+ android_details: {
468
+ android_package: androidPackage,
469
+ version: versionName,
470
+ version_code: versionCode,
471
+ min_sdk: minSdkVersion,
472
+ target_sdk: targetSdkVersion,
473
+ cert_fingerprint: certificateFingerprint,
474
+ permissions: asStringArray(permissions),
475
+ locales: asStringArray(locales),
476
+ },
477
+ },
478
+ i18n: {
479
+ "en-US": {
480
+ "1": longDescription,
481
+ "2": newInVersion,
482
+ "4": releaseName,
483
+ "5": shortDescription.slice(0, 50),
484
+ },
485
+ },
486
+ },
487
+ __origin: sourceKind,
488
+ };
489
+ }