@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,508 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ jest,
8
+ } from "@jest/globals";
9
+
10
+ import {
11
+ buildReleaseMetadataDocument,
12
+ type ReleaseMetadataPortalClient,
13
+ } from "../releaseMetadata.js";
14
+
15
+ function makePortalClient(options?: {
16
+ fetchRemoteFile?: Record<
17
+ string,
18
+ {
19
+ data: string;
20
+ fileName: string;
21
+ mimeType: string;
22
+ }
23
+ >;
24
+ }): jest.Mocked<ReleaseMetadataPortalClient> {
25
+ return {
26
+ fetchRemoteFile: jest.fn(async (input) => {
27
+ if (!options?.fetchRemoteFile?.[input.url]) {
28
+ throw new Error(`Unexpected remote file fetch for ${input.url}`);
29
+ }
30
+
31
+ return options.fetchRemoteFile[input.url]!;
32
+ }),
33
+ createUploadTarget: jest.fn(),
34
+ };
35
+ }
36
+
37
+ describe("buildReleaseMetadataDocument", () => {
38
+ let originalFetch: typeof fetch;
39
+ let fetchMock: jest.MockedFunction<typeof fetch>;
40
+
41
+ beforeEach(() => {
42
+ originalFetch = global.fetch;
43
+ fetchMock = jest.fn<typeof fetch>();
44
+ global.fetch = fetchMock as typeof fetch;
45
+ });
46
+
47
+ afterEach(() => {
48
+ global.fetch = originalFetch;
49
+ jest.restoreAllMocks();
50
+ });
51
+
52
+ it("reuses existing R2 media URLs without reuploading them", async () => {
53
+ const portal = makePortalClient({
54
+ fetchRemoteFile: {
55
+ "https://r2.solanamobiledappstore.com/a/icon.png": {
56
+ data: Buffer.from(
57
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aR3sAAAAASUVORK5CYII=",
58
+ "base64"
59
+ ).toString("base64"),
60
+ fileName: "icon.png",
61
+ mimeType: "image/png",
62
+ },
63
+ "https://r2.solanamobiledappstore.com/a/preview.png": {
64
+ data: Buffer.from(
65
+ "iVBORw0KGgoAAAANSUhEUgAAAAIAAAADCAIAAAA2iEnWAAAAFElEQVR42mP8z8AARMAgYGJAAwA5WQYB8m8VXwAAAABJRU5ErkJggg==",
66
+ "base64"
67
+ ).toString("base64"),
68
+ fileName: "preview.png",
69
+ mimeType: "image/png",
70
+ },
71
+ },
72
+ });
73
+
74
+ const metadata = (await buildReleaseMetadataDocument(
75
+ portal,
76
+ {
77
+ ingestionSessionId: "ingestion-1",
78
+ publicationSessionId: "publication-1",
79
+ releaseId: "release-1",
80
+ release: {
81
+ id: "release-1",
82
+ androidPackage: "com.example.app",
83
+ versionName: "1.0.1",
84
+ versionCode: 2,
85
+ minSdkVersion: 26,
86
+ targetSdkVersion: 36,
87
+ certificateFingerprint: "fingerprint",
88
+ permissions: ["android.permission.INTERNET"],
89
+ locales: ["en-US"],
90
+ shortDescription: "Pay with QR, NFC, Bluetooth, and .skr",
91
+ longDescription: "Long description",
92
+ localizedName: "Seeker PAY",
93
+ newInVersion: "UI improvements",
94
+ },
95
+ dapp: {
96
+ id: "dapp-1",
97
+ dappName: "Seeker PAY",
98
+ subtitle: "Pay with QR, NFC, Bluetooth, and .skr",
99
+ description: "Long description",
100
+ androidPackage: "com.example.app",
101
+ dappIconUrl: "https://r2.solanamobiledappstore.com/a/icon.png",
102
+ dappPreviewUrls: [
103
+ "https://r2.solanamobiledappstore.com/a/preview.png",
104
+ ],
105
+ appWebsite: "https://example.com",
106
+ contactEmail: "contact@example.com",
107
+ supportEmail: "support@example.com",
108
+ languages: ["en-US"],
109
+ licenseUrl: "https://example.com/license",
110
+ copyrightUrl: "https://example.com/copyright",
111
+ privacyPolicyUrl: "https://example.com/privacy",
112
+ walletAddress: "publisher-wallet",
113
+ nftMintAddress: "app-mint",
114
+ },
115
+ publisher: {
116
+ id: "publisher-1",
117
+ type: "organization",
118
+ name: "Publisher",
119
+ website: "https://example.com",
120
+ email: "contact@example.com",
121
+ supportEmail: "support@example.com",
122
+ },
123
+ installFile: {
124
+ uri: "https://example.com/release.apk",
125
+ mimeType: "application/vnd.android.package-archive",
126
+ size: 123456,
127
+ sha256: "apk-hash",
128
+ },
129
+ signerAuthority: {
130
+ dappWalletAddress: "publisher-wallet",
131
+ collectionAuthority: "publisher-wallet",
132
+ appMintAddress: "app-mint",
133
+ sameSignerRequired: true,
134
+ acceptedSignerRoles: ["publisher"],
135
+ },
136
+ },
137
+ "portal"
138
+ )) as Record<string, any>;
139
+
140
+ expect(metadata.image).toBe(
141
+ "https://r2.solanamobiledappstore.com/a/icon.png"
142
+ );
143
+ expect(metadata.extensions.solana_dapp_store.media).toEqual([
144
+ {
145
+ mime: "image/png",
146
+ purpose: "icon",
147
+ uri: "https://r2.solanamobiledappstore.com/a/icon.png",
148
+ width: 1,
149
+ height: 1,
150
+ sha256: expect.any(String),
151
+ },
152
+ {
153
+ mime: "image/png",
154
+ purpose: "screenshot",
155
+ uri: "https://r2.solanamobiledappstore.com/a/preview.png",
156
+ width: 2,
157
+ height: 3,
158
+ sha256: expect.any(String),
159
+ },
160
+ ]);
161
+ expect(
162
+ metadata.extensions.solana_dapp_store.android_details.target_sdk
163
+ ).toBe(36);
164
+ expect(
165
+ metadata.extensions.solana_dapp_store.android_details.cert_fingerprint
166
+ ).toBe("fingerprint");
167
+ expect(portal.fetchRemoteFile).toHaveBeenCalledTimes(2);
168
+ expect(portal.createUploadTarget).not.toHaveBeenCalled();
169
+ expect(fetchMock).not.toHaveBeenCalled();
170
+ });
171
+
172
+ it("mirrors portal-hosted release media to R2 before emitting metadata", async () => {
173
+ fetchMock.mockResolvedValue(new Response("", { status: 200 }));
174
+
175
+ const iconUrl =
176
+ "https://dev-portal-uploads-prod-854862047012.s3.amazonaws.com/logos/icon.png";
177
+ const previewUrl =
178
+ "https://dev-portal-uploads-prod-854862047012.s3.amazonaws.com/previews/preview.png";
179
+ const portal = makePortalClient({
180
+ fetchRemoteFile: {
181
+ [iconUrl]: {
182
+ data: Buffer.from(
183
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aR3sAAAAASUVORK5CYII=",
184
+ "base64"
185
+ ).toString("base64"),
186
+ fileName: "icon.png",
187
+ mimeType: "image/png",
188
+ },
189
+ [previewUrl]: {
190
+ data: Buffer.from(
191
+ "iVBORw0KGgoAAAANSUhEUgAAAAIAAAADCAIAAAA2iEnWAAAAFElEQVR42mP8z8AARMAgYGJAAwA5WQYB8m8VXwAAAABJRU5ErkJggg==",
192
+ "base64"
193
+ ).toString("base64"),
194
+ fileName: "preview.png",
195
+ mimeType: "image/png",
196
+ },
197
+ },
198
+ });
199
+
200
+ portal.createUploadTarget
201
+ .mockResolvedValueOnce({
202
+ uploadUrl: "https://upload.example.com/icon",
203
+ key: "icon",
204
+ providerId: "provider-1",
205
+ publicUrl: "https://r2.solanamobiledappstore.com/a/r2-icon.png",
206
+ })
207
+ .mockResolvedValueOnce({
208
+ uploadUrl: "https://upload.example.com/preview",
209
+ key: "preview",
210
+ providerId: "provider-1",
211
+ publicUrl: "https://r2.solanamobiledappstore.com/a/r2-preview.png",
212
+ });
213
+
214
+ const metadata = (await buildReleaseMetadataDocument(
215
+ portal,
216
+ {
217
+ ingestionSessionId: "ingestion-1",
218
+ publicationSessionId: "publication-1",
219
+ releaseId: "release-1",
220
+ release: {
221
+ id: "release-1",
222
+ androidPackage: "com.example.app",
223
+ versionName: "1.0.1",
224
+ versionCode: 2,
225
+ minSdkVersion: 26,
226
+ targetSdkVersion: 36,
227
+ certificateFingerprint: "fingerprint",
228
+ permissions: ["android.permission.INTERNET"],
229
+ locales: ["en-US"],
230
+ shortDescription: "Pay with QR, NFC, Bluetooth, and .skr",
231
+ longDescription: "Long description",
232
+ localizedName: "Seeker PAY",
233
+ newInVersion: "UI improvements",
234
+ },
235
+ dapp: {
236
+ id: "dapp-1",
237
+ dappName: "Seeker PAY",
238
+ subtitle: "Pay with QR, NFC, Bluetooth, and .skr",
239
+ description: "Long description",
240
+ androidPackage: "com.example.app",
241
+ dappIconUrl: iconUrl,
242
+ dappPreviewUrls: [previewUrl],
243
+ appWebsite: "https://example.com",
244
+ contactEmail: "contact@example.com",
245
+ supportEmail: "support@example.com",
246
+ languages: ["en-US"],
247
+ licenseUrl: "https://example.com/license",
248
+ copyrightUrl: "https://example.com/copyright",
249
+ privacyPolicyUrl: "https://example.com/privacy",
250
+ walletAddress: "publisher-wallet",
251
+ nftMintAddress: "app-mint",
252
+ },
253
+ publisher: {
254
+ id: "publisher-1",
255
+ type: "organization",
256
+ name: "Publisher",
257
+ website: "https://example.com",
258
+ email: "contact@example.com",
259
+ supportEmail: "support@example.com",
260
+ },
261
+ installFile: {
262
+ uri: "https://example.com/release.apk",
263
+ mimeType: "application/vnd.android.package-archive",
264
+ size: 123456,
265
+ sha256: "apk-hash",
266
+ },
267
+ signerAuthority: {
268
+ dappWalletAddress: "publisher-wallet",
269
+ collectionAuthority: "publisher-wallet",
270
+ appMintAddress: "app-mint",
271
+ sameSignerRequired: true,
272
+ acceptedSignerRoles: ["publisher"],
273
+ },
274
+ },
275
+ "portal"
276
+ )) as Record<string, any>;
277
+
278
+ expect(metadata.image).toBe(
279
+ "https://r2.solanamobiledappstore.com/a/r2-icon.png"
280
+ );
281
+ expect(metadata.extensions.solana_dapp_store.media).toMatchObject([
282
+ {
283
+ mime: "image/png",
284
+ purpose: "icon",
285
+ uri: "https://r2.solanamobiledappstore.com/a/r2-icon.png",
286
+ width: 1,
287
+ height: 1,
288
+ sha256: expect.any(String),
289
+ },
290
+ {
291
+ mime: "image/png",
292
+ purpose: "screenshot",
293
+ uri: "https://r2.solanamobiledappstore.com/a/r2-preview.png",
294
+ width: 2,
295
+ height: 3,
296
+ sha256: expect.any(String),
297
+ },
298
+ ]);
299
+ expect(portal.fetchRemoteFile).toHaveBeenCalledTimes(2);
300
+ expect(portal.createUploadTarget).toHaveBeenCalledTimes(2);
301
+ expect(fetchMock).toHaveBeenCalledTimes(2);
302
+ });
303
+
304
+ it("normalizes schemeless media URLs before fetching them", async () => {
305
+ const iconUrl = "r2.solanamobiledappstore.com/a/icon.png";
306
+ const normalizedIconUrl = `https://${iconUrl}`;
307
+ const portal = makePortalClient({
308
+ fetchRemoteFile: {
309
+ [normalizedIconUrl]: {
310
+ data: Buffer.from(
311
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aR3sAAAAASUVORK5CYII=",
312
+ "base64"
313
+ ).toString("base64"),
314
+ fileName: "icon.png",
315
+ mimeType: "image/png",
316
+ },
317
+ },
318
+ });
319
+
320
+ const metadata = (await buildReleaseMetadataDocument(
321
+ portal,
322
+ {
323
+ ingestionSessionId: "ingestion-1",
324
+ publicationSessionId: "publication-1",
325
+ releaseId: "release-1",
326
+ release: {
327
+ id: "release-1",
328
+ androidPackage: "com.example.app",
329
+ versionName: "1.0.1",
330
+ versionCode: 2,
331
+ minSdkVersion: 26,
332
+ targetSdkVersion: 36,
333
+ certificateFingerprint: "fingerprint",
334
+ permissions: ["android.permission.INTERNET"],
335
+ locales: ["en-US"],
336
+ shortDescription: "Pay with QR, NFC, Bluetooth, and .skr",
337
+ longDescription: "Long description",
338
+ localizedName: "Seeker PAY",
339
+ newInVersion: "UI improvements",
340
+ },
341
+ dapp: {
342
+ id: "dapp-1",
343
+ dappName: "Seeker PAY",
344
+ subtitle: "Pay with QR, NFC, Bluetooth, and .skr",
345
+ description: "Long description",
346
+ androidPackage: "com.example.app",
347
+ dappIconUrl: iconUrl,
348
+ dappPreviewUrls: [],
349
+ appWebsite: "https://example.com",
350
+ contactEmail: "contact@example.com",
351
+ supportEmail: "support@example.com",
352
+ languages: ["en-US"],
353
+ licenseUrl: "https://example.com/license",
354
+ copyrightUrl: "https://example.com/copyright",
355
+ privacyPolicyUrl: "https://example.com/privacy",
356
+ walletAddress: "publisher-wallet",
357
+ nftMintAddress: "app-mint",
358
+ },
359
+ publisher: {
360
+ id: "publisher-1",
361
+ type: "organization",
362
+ name: "Publisher",
363
+ website: "https://example.com",
364
+ email: "contact@example.com",
365
+ supportEmail: "support@example.com",
366
+ },
367
+ installFile: {
368
+ uri: "https://example.com/release.apk",
369
+ mimeType: "application/vnd.android.package-archive",
370
+ size: 123456,
371
+ sha256: "apk-hash",
372
+ },
373
+ signerAuthority: {
374
+ dappWalletAddress: "publisher-wallet",
375
+ collectionAuthority: "publisher-wallet",
376
+ appMintAddress: "app-mint",
377
+ sameSignerRequired: true,
378
+ acceptedSignerRoles: ["publisher"],
379
+ },
380
+ },
381
+ "portal"
382
+ )) as Record<string, any>;
383
+
384
+ expect(metadata.image).toBe(normalizedIconUrl);
385
+ expect(portal.fetchRemoteFile).toHaveBeenCalledWith(
386
+ expect.objectContaining({
387
+ url: normalizedIconUrl,
388
+ })
389
+ );
390
+ expect(portal.createUploadTarget).not.toHaveBeenCalled();
391
+ expect(fetchMock).not.toHaveBeenCalled();
392
+ });
393
+
394
+ it("does not force feature graphics to image/png when the source is jpeg", async () => {
395
+ fetchMock.mockResolvedValue(new Response("", { status: 200 }));
396
+
397
+ const iconUrl = "https://r2.solanamobiledappstore.com/a/icon.png";
398
+ const featureGraphicUrl =
399
+ "https://dev-portal-uploads-prod-854862047012.s3.amazonaws.com/previews/feature.jpg";
400
+ const portal = makePortalClient({
401
+ fetchRemoteFile: {
402
+ [iconUrl]: {
403
+ data: Buffer.from(
404
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aR3sAAAAASUVORK5CYII=",
405
+ "base64"
406
+ ).toString("base64"),
407
+ fileName: "icon.png",
408
+ mimeType: "image/png",
409
+ },
410
+ [featureGraphicUrl]: {
411
+ data: Buffer.from(
412
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aR3sAAAAASUVORK5CYII=",
413
+ "base64"
414
+ ).toString("base64"),
415
+ fileName: "feature.jpg",
416
+ mimeType: "image/jpeg",
417
+ },
418
+ },
419
+ });
420
+
421
+ portal.createUploadTarget.mockResolvedValueOnce({
422
+ uploadUrl: "https://upload.example.com/feature",
423
+ key: "feature",
424
+ providerId: "provider-1",
425
+ publicUrl: "https://r2.solanamobiledappstore.com/a/r2-feature.jpg",
426
+ });
427
+
428
+ const metadata = (await buildReleaseMetadataDocument(
429
+ portal,
430
+ {
431
+ ingestionSessionId: "ingestion-1",
432
+ publicationSessionId: "publication-1",
433
+ releaseId: "release-1",
434
+ release: {
435
+ id: "release-1",
436
+ androidPackage: "com.example.app",
437
+ versionName: "1.0.1",
438
+ versionCode: 2,
439
+ minSdkVersion: 26,
440
+ targetSdkVersion: 36,
441
+ certificateFingerprint: "fingerprint",
442
+ permissions: ["android.permission.INTERNET"],
443
+ locales: ["en-US"],
444
+ shortDescription: "Pay with QR, NFC, Bluetooth, and .skr",
445
+ longDescription: "Long description",
446
+ localizedName: "Seeker PAY",
447
+ newInVersion: "UI improvements",
448
+ },
449
+ dapp: {
450
+ id: "dapp-1",
451
+ dappName: "Seeker PAY",
452
+ subtitle: "Pay with QR, NFC, Bluetooth, and .skr",
453
+ description: "Long description",
454
+ androidPackage: "com.example.app",
455
+ dappIconUrl: iconUrl,
456
+ dappPreviewUrls: [],
457
+ featureGraphicUrl,
458
+ appWebsite: "https://example.com",
459
+ contactEmail: "contact@example.com",
460
+ supportEmail: "support@example.com",
461
+ languages: ["en-US"],
462
+ licenseUrl: "https://example.com/license",
463
+ copyrightUrl: "https://example.com/copyright",
464
+ privacyPolicyUrl: "https://example.com/privacy",
465
+ walletAddress: "publisher-wallet",
466
+ nftMintAddress: "app-mint",
467
+ },
468
+ publisher: {
469
+ id: "publisher-1",
470
+ type: "organization",
471
+ name: "Publisher",
472
+ website: "https://example.com",
473
+ email: "contact@example.com",
474
+ supportEmail: "support@example.com",
475
+ },
476
+ installFile: {
477
+ uri: "https://example.com/release.apk",
478
+ mimeType: "application/vnd.android.package-archive",
479
+ size: 123456,
480
+ sha256: "apk-hash",
481
+ },
482
+ signerAuthority: {
483
+ dappWalletAddress: "publisher-wallet",
484
+ collectionAuthority: "publisher-wallet",
485
+ appMintAddress: "app-mint",
486
+ sameSignerRequired: true,
487
+ acceptedSignerRoles: ["publisher"],
488
+ },
489
+ },
490
+ "portal"
491
+ )) as Record<string, any>;
492
+
493
+ const featureMedia = metadata.extensions.solana_dapp_store.media.find(
494
+ (item: Record<string, any>) => item.purpose === "featureGraphic"
495
+ );
496
+
497
+ expect(featureMedia).toMatchObject({
498
+ mime: "image/jpeg",
499
+ uri: "https://r2.solanamobiledappstore.com/a/r2-feature.jpg",
500
+ });
501
+ expect(portal.fetchRemoteFile).toHaveBeenCalledWith(
502
+ expect.objectContaining({
503
+ url: featureGraphicUrl,
504
+ expectedMimeType: undefined,
505
+ })
506
+ );
507
+ });
508
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+
3
+ import { mapBackendBundleToPublicationBundle } from '../translators.js';
4
+
5
+ describe('mapBackendBundleToPublicationBundle', () => {
6
+ it('keeps localized short-description fallback distinct from metadata shortDescription', () => {
7
+ const description = 'A'.repeat(80);
8
+
9
+ const bundle = mapBackendBundleToPublicationBundle(
10
+ {
11
+ dapp: {
12
+ dappName: 'Example dApp',
13
+ description,
14
+ subtitle: 'Portal subtitle',
15
+ },
16
+ release: {},
17
+ publisher: {},
18
+ installFile: {},
19
+ signerAuthority: {},
20
+ },
21
+ '',
22
+ 'portal'
23
+ );
24
+
25
+ expect(bundle.metadata.shortDescription).toBe('Portal subtitle');
26
+ expect(bundle.metadata.localizedStrings[0].shortDescription).toBe(
27
+ description.slice(0, 50)
28
+ );
29
+ });
30
+
31
+ it('preserves required release metadata fields from the backend bundle', () => {
32
+ const bundle = mapBackendBundleToPublicationBundle(
33
+ {
34
+ dapp: {
35
+ id: 'dapp-1',
36
+ dappName: 'Example dApp',
37
+ description: 'Long description',
38
+ walletAddress: 'wallet-1',
39
+ androidPackage: 'com.example.app',
40
+ dappIconUrl: 'https://example.com/icon.png',
41
+ dappPreviewUrls: ['https://example.com/preview.png'],
42
+ editorsChoiceGraphicUrl: 'https://example.com/editor.png',
43
+ languages: ['en-US'],
44
+ },
45
+ release: {
46
+ id: 'release-1',
47
+ dappId: 'dapp-1',
48
+ releaseFileUrl: 'https://example.com/release.apk',
49
+ releaseFileName: 'release.apk',
50
+ releaseFileSize: 123,
51
+ releaseFileHash: 'apk-hash',
52
+ versionCode: 42,
53
+ versionName: '1.0.42',
54
+ androidPackage: 'com.example.app',
55
+ minSdkVersion: 26,
56
+ targetSdkVersion: 35,
57
+ permissions: ['android.permission.INTERNET'],
58
+ locales: ['en-US'],
59
+ certificateFingerprint: 'fingerprint',
60
+ shortDescription: 'Short description',
61
+ longDescription: 'Long description',
62
+ localizedName: 'Example App',
63
+ newInVersion: 'Bug fixes',
64
+ },
65
+ publisher: {},
66
+ installFile: {},
67
+ signerAuthority: {},
68
+ },
69
+ 'https://example.com/metadata.json',
70
+ 'portal'
71
+ );
72
+
73
+ expect(bundle.dapp.editorsChoiceGraphicUrl).toBe(
74
+ 'https://example.com/editor.png'
75
+ );
76
+ expect(bundle.release.targetSdkVersion).toBe(35);
77
+ expect(bundle.release.certificateFingerprint).toBe('fingerprint');
78
+ expect(bundle.release.permissions).toEqual(['android.permission.INTERNET']);
79
+ expect(bundle.release.locales).toEqual(['en-US']);
80
+ expect(bundle.release.releaseFileHash).toBe('apk-hash');
81
+ });
82
+ });