@solana-mobile/dapp-store-cli 0.4.3 → 0.5.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.
- package/lib/CliSetup.js +517 -444
- package/lib/CliUtils.js +1 -1
- package/lib/commands/create/CreateCliApp.js +12 -3
- package/lib/commands/create/CreateCliPublisher.js +10 -5
- package/lib/commands/create/CreateCliRelease.js +12 -7
- package/lib/config/PublishDetails.js +54 -16
- package/lib/index.js +1 -0
- package/lib/package.json +2 -2
- package/package.json +2 -2
- package/src/CliSetup.ts +10 -12
- package/src/CliUtils.ts +1 -1
- package/src/commands/create/CreateCliApp.ts +1 -1
- package/src/commands/create/CreateCliPublisher.ts +1 -1
- package/src/commands/create/CreateCliRelease.ts +1 -3
- package/src/config/PublishDetails.ts +17 -1
- package/src/index.ts +2 -0
package/lib/CliUtils.js
CHANGED
|
@@ -159,7 +159,7 @@ export var Constants = function Constants() {
|
|
|
159
159
|
"use strict";
|
|
160
160
|
_class_call_check(this, Constants);
|
|
161
161
|
};
|
|
162
|
-
_define_property(Constants, "CLI_VERSION", "0.
|
|
162
|
+
_define_property(Constants, "CLI_VERSION", "0.5.1");
|
|
163
163
|
_define_property(Constants, "CONFIG_FILE_NAME", "config.yaml");
|
|
164
164
|
_define_property(Constants, "DEFAULT_RPC_DEVNET", "https://api.devnet.solana.com");
|
|
165
165
|
_define_property(Constants, "getConfigFilePath", function() {
|
|
@@ -218,13 +218,22 @@ export var createAppCommand = function() {
|
|
|
218
218
|
];
|
|
219
219
|
case 2:
|
|
220
220
|
appAddress = _state.sent().appAddress;
|
|
221
|
-
if (
|
|
221
|
+
if (!!dryRun) return [
|
|
222
|
+
3,
|
|
223
|
+
4
|
|
224
|
+
];
|
|
225
|
+
return [
|
|
226
|
+
4,
|
|
222
227
|
writeToPublishDetails({
|
|
223
228
|
app: {
|
|
224
229
|
address: appAddress
|
|
225
230
|
}
|
|
226
|
-
})
|
|
227
|
-
|
|
231
|
+
})
|
|
232
|
+
];
|
|
233
|
+
case 3:
|
|
234
|
+
_state.sent();
|
|
235
|
+
_state.label = 4;
|
|
236
|
+
case 4:
|
|
228
237
|
return [
|
|
229
238
|
2,
|
|
230
239
|
{
|
|
@@ -218,11 +218,16 @@ export var createPublisherCommand = function() {
|
|
|
218
218
|
case 2:
|
|
219
219
|
publisherAddress = _state.sent().publisherAddress;
|
|
220
220
|
// TODO(sdlaver): dry-run should not modify config
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
return [
|
|
222
|
+
4,
|
|
223
|
+
writeToPublishDetails({
|
|
224
|
+
publisher: {
|
|
225
|
+
address: publisherAddress
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
];
|
|
229
|
+
case 3:
|
|
230
|
+
_state.sent();
|
|
226
231
|
return [
|
|
227
232
|
2,
|
|
228
233
|
{
|
|
@@ -230,7 +230,7 @@ export var createReleaseCommand = function() {
|
|
|
230
230
|
_ref = _state.sent(), release = _ref.release, app = _ref.app, publisher = _ref.publisher;
|
|
231
231
|
if (!!dryRun) return [
|
|
232
232
|
3,
|
|
233
|
-
|
|
233
|
+
4
|
|
234
234
|
];
|
|
235
235
|
return [
|
|
236
236
|
4,
|
|
@@ -246,18 +246,23 @@ export var createReleaseCommand = function() {
|
|
|
246
246
|
];
|
|
247
247
|
case 2:
|
|
248
248
|
releaseAddress = _state.sent().releaseAddress;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
249
|
+
return [
|
|
250
|
+
4,
|
|
251
|
+
writeToPublishDetails({
|
|
252
|
+
release: {
|
|
253
|
+
address: releaseAddress
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
];
|
|
257
|
+
case 3:
|
|
258
|
+
_state.sent();
|
|
254
259
|
return [
|
|
255
260
|
2,
|
|
256
261
|
{
|
|
257
262
|
releaseAddress: releaseAddress
|
|
258
263
|
}
|
|
259
264
|
];
|
|
260
|
-
case
|
|
265
|
+
case 4:
|
|
261
266
|
return [
|
|
262
267
|
2
|
|
263
268
|
];
|
|
@@ -456,15 +456,15 @@ var checkIconDimensions = function() {
|
|
|
456
456
|
}();
|
|
457
457
|
var getAndroidDetails = function() {
|
|
458
458
|
var _ref = _async_to_generator(function(aaptDir, apkPath) {
|
|
459
|
-
var stdout, appPackage, versionCode, versionName, minSdk, permissions, locales, _locales_values, localeArray, localesSrc, _appPackage_, _minSdk_, _versionCode_, _versionName_, e;
|
|
459
|
+
var stdout, appPackage, versionCode, versionName, minSdk, permissions, locales, _locales_values, localeArray, localesSrc, _appPackage_, _minSdk_, _versionCode_, _versionName_, _tmp, e;
|
|
460
460
|
return _ts_generator(this, function(_state) {
|
|
461
461
|
switch(_state.label){
|
|
462
462
|
case 0:
|
|
463
463
|
_state.trys.push([
|
|
464
464
|
0,
|
|
465
|
-
|
|
465
|
+
3,
|
|
466
466
|
,
|
|
467
|
-
|
|
467
|
+
4
|
|
468
468
|
]);
|
|
469
469
|
return [
|
|
470
470
|
4,
|
|
@@ -488,23 +488,27 @@ var getAndroidDetails = function() {
|
|
|
488
488
|
if (localeArray.length >= 60) {
|
|
489
489
|
showMessage("The bundle apk claims supports for following locales", "Claim for supported locales::\n" + localeArray + "\nIf this release does not support all these locales the release may be rejected" + "\nSee details at https://developer.android.com/guide/topics/resources/multilingual-support#design for configuring the supported locales", "warning");
|
|
490
490
|
}
|
|
491
|
+
_tmp = {
|
|
492
|
+
android_package: (_appPackage_ = appPackage === null || appPackage === void 0 ? void 0 : appPackage[1]) !== null && _appPackage_ !== void 0 ? _appPackage_ : "",
|
|
493
|
+
min_sdk: parseInt((_minSdk_ = minSdk === null || minSdk === void 0 ? void 0 : minSdk[1]) !== null && _minSdk_ !== void 0 ? _minSdk_ : "0", 10),
|
|
494
|
+
version_code: parseInt((_versionCode_ = versionCode === null || versionCode === void 0 ? void 0 : versionCode[1]) !== null && _versionCode_ !== void 0 ? _versionCode_ : "0", 10),
|
|
495
|
+
version: (_versionName_ = versionName === null || versionName === void 0 ? void 0 : versionName[1]) !== null && _versionName_ !== void 0 ? _versionName_ : "0"
|
|
496
|
+
};
|
|
491
497
|
return [
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
android_package: (_appPackage_ = appPackage === null || appPackage === void 0 ? void 0 : appPackage[1]) !== null && _appPackage_ !== void 0 ? _appPackage_ : "",
|
|
495
|
-
min_sdk: parseInt((_minSdk_ = minSdk === null || minSdk === void 0 ? void 0 : minSdk[1]) !== null && _minSdk_ !== void 0 ? _minSdk_ : "0", 10),
|
|
496
|
-
version_code: parseInt((_versionCode_ = versionCode === null || versionCode === void 0 ? void 0 : versionCode[1]) !== null && _versionCode_ !== void 0 ? _versionCode_ : "0", 10),
|
|
497
|
-
version: (_versionName_ = versionName === null || versionName === void 0 ? void 0 : versionName[1]) !== null && _versionName_ !== void 0 ? _versionName_ : "0",
|
|
498
|
-
permissions: permissions.flatMap(function(permission) {
|
|
499
|
-
return permission[1];
|
|
500
|
-
}),
|
|
501
|
-
locales: localeArray
|
|
502
|
-
}
|
|
498
|
+
4,
|
|
499
|
+
extractCertFingerprint(aaptDir, apkPath)
|
|
503
500
|
];
|
|
504
501
|
case 2:
|
|
502
|
+
return [
|
|
503
|
+
2,
|
|
504
|
+
(_tmp.cert_fingerprint = _state.sent(), _tmp.permissions = permissions.flatMap(function(permission) {
|
|
505
|
+
return permission[1];
|
|
506
|
+
}), _tmp.locales = localeArray, _tmp)
|
|
507
|
+
];
|
|
508
|
+
case 3:
|
|
505
509
|
e = _state.sent();
|
|
506
510
|
throw new Error("There was an error parsing your APK. Please ensure you have provided a valid Android tools directory containing AAPT2.");
|
|
507
|
-
case
|
|
511
|
+
case 4:
|
|
508
512
|
return [
|
|
509
513
|
2
|
|
510
514
|
];
|
|
@@ -515,6 +519,38 @@ var getAndroidDetails = function() {
|
|
|
515
519
|
return _ref.apply(this, arguments);
|
|
516
520
|
};
|
|
517
521
|
}();
|
|
522
|
+
export var extractCertFingerprint = function() {
|
|
523
|
+
var _ref = _async_to_generator(function(aaptDir, apkPath) {
|
|
524
|
+
var stdout, regex, match;
|
|
525
|
+
return _ts_generator(this, function(_state) {
|
|
526
|
+
switch(_state.label){
|
|
527
|
+
case 0:
|
|
528
|
+
return [
|
|
529
|
+
4,
|
|
530
|
+
runExec("".concat(aaptDir, '/apksigner verify --print-certs -v "').concat(apkPath, '"'))
|
|
531
|
+
];
|
|
532
|
+
case 1:
|
|
533
|
+
stdout = _state.sent().stdout;
|
|
534
|
+
regex = /Signer #1 certificate SHA-256 digest:\s*([a-fA-F0-9]+)/;
|
|
535
|
+
match = stdout.match(regex);
|
|
536
|
+
if (match && match[1]) {
|
|
537
|
+
return [
|
|
538
|
+
2,
|
|
539
|
+
match[1]
|
|
540
|
+
];
|
|
541
|
+
} else {
|
|
542
|
+
throw new Error("Could not obtain cert fingerprint");
|
|
543
|
+
}
|
|
544
|
+
return [
|
|
545
|
+
2
|
|
546
|
+
];
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
return function extractCertFingerprint(aaptDir, apkPath) {
|
|
551
|
+
return _ref.apply(this, arguments);
|
|
552
|
+
};
|
|
553
|
+
}();
|
|
518
554
|
export var writeToPublishDetails = function() {
|
|
519
555
|
var _ref = _async_to_generator(function(param) {
|
|
520
556
|
var publisher, app, release, currentConfig, _publisher_address, _app_address, _release_address, newConfig;
|
|
@@ -542,7 +578,9 @@ export var writeToPublishDetails = function() {
|
|
|
542
578
|
}),
|
|
543
579
|
solana_mobile_dapp_publisher_portal: currentConfig.solana_mobile_dapp_publisher_portal
|
|
544
580
|
};
|
|
545
|
-
fs.writeFileSync(Constants.getConfigFilePath(), dump(newConfig
|
|
581
|
+
fs.writeFileSync(Constants.getConfigFilePath(), dump(newConfig, {
|
|
582
|
+
lineWidth: -1
|
|
583
|
+
}));
|
|
546
584
|
return [
|
|
547
585
|
2
|
|
548
586
|
];
|
package/lib/index.js
CHANGED
package/lib/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solana-mobile/dapp-store-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@aws-sdk/client-s3": "^3.321.1",
|
|
55
55
|
"@metaplex-foundation/js-plugin-aws": "^0.18.3",
|
|
56
|
-
"@solana-mobile/dapp-store-publishing-tools": "workspace:0.
|
|
56
|
+
"@solana-mobile/dapp-store-publishing-tools": "workspace:0.5.1",
|
|
57
57
|
"@solana/web3.js": "1.68.0",
|
|
58
58
|
"@types/semver": "^7.3.13",
|
|
59
59
|
"ajv": "^8.11.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solana-mobile/dapp-store-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@aws-sdk/client-s3": "^3.321.1",
|
|
48
48
|
"@metaplex-foundation/js-plugin-aws": "^0.18.3",
|
|
49
|
-
"@solana-mobile/dapp-store-publishing-tools": "0.
|
|
49
|
+
"@solana-mobile/dapp-store-publishing-tools": "0.5.1",
|
|
50
50
|
"@solana/web3.js": "1.68.0",
|
|
51
51
|
"@types/semver": "^7.3.13",
|
|
52
52
|
"ajv": "^8.11.0",
|
package/src/CliSetup.ts
CHANGED
|
@@ -48,7 +48,6 @@ function resolveBuildToolsPath(buildToolsPath: string | undefined) {
|
|
|
48
48
|
function latestReleaseMessage() {
|
|
49
49
|
showMessage(
|
|
50
50
|
`Publishing Tools Version ${ Constants.CLI_VERSION }`,
|
|
51
|
-
"- S3 bucket-based storage support added \n" +
|
|
52
51
|
"- short_description value reduced to 30 character limit",
|
|
53
52
|
"warning"
|
|
54
53
|
);
|
|
@@ -61,6 +60,7 @@ async function tryWithErrorMessage(block: () => Promise<any>) {
|
|
|
61
60
|
const errorMsg = (e as Error | null)?.message ?? "";
|
|
62
61
|
|
|
63
62
|
showMessage("Error", errorMsg, "error");
|
|
63
|
+
process.exit(-1)
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -73,7 +73,7 @@ export const initCliCmd = mainCli
|
|
|
73
73
|
.command("init")
|
|
74
74
|
.description("First-time initialization of tooling configuration")
|
|
75
75
|
.action(async () => {
|
|
76
|
-
tryWithErrorMessage(async () => {
|
|
76
|
+
await tryWithErrorMessage(async () => {
|
|
77
77
|
const msg = initScaffold();
|
|
78
78
|
|
|
79
79
|
showMessage("Initialized", msg);
|
|
@@ -95,7 +95,7 @@ export const createPublisherCliCmd = createCliCmd
|
|
|
95
95
|
.option("-d, --dry-run", "Flag for dry run. Doesn't mint an NFT")
|
|
96
96
|
.option("-s, --storage-config <storage-config>", "Provide alternative storage configuration details")
|
|
97
97
|
.action(async ({ keypair, url, dryRun, storageConfig }) => {
|
|
98
|
-
tryWithErrorMessage(async () => {
|
|
98
|
+
await tryWithErrorMessage(async () => {
|
|
99
99
|
latestReleaseMessage();
|
|
100
100
|
await checkForSelfUpdate();
|
|
101
101
|
|
|
@@ -126,7 +126,7 @@ export const createAppCliCmd = createCliCmd
|
|
|
126
126
|
.option("-d, --dry-run", "Flag for dry run. Doesn't mint an NFT")
|
|
127
127
|
.option("-s, --storage-config <storage-config>", "Provide alternative storage configuration details")
|
|
128
128
|
.action(async ({ publisherMintAddress, keypair, url, dryRun, storageConfig }) => {
|
|
129
|
-
tryWithErrorMessage(async () => {
|
|
129
|
+
await tryWithErrorMessage(async () => {
|
|
130
130
|
latestReleaseMessage();
|
|
131
131
|
await checkForSelfUpdate();
|
|
132
132
|
|
|
@@ -173,7 +173,7 @@ export const createReleaseCliCmd = createCliCmd
|
|
|
173
173
|
)
|
|
174
174
|
.option("-s, --storage-config <storage-config>", "Provide alternative storage configuration details")
|
|
175
175
|
.action(async ({ appMintAddress, keypair, url, dryRun, buildToolsPath, storageConfig }) => {
|
|
176
|
-
|
|
176
|
+
await tryWithErrorMessage(async () => {
|
|
177
177
|
latestReleaseMessage();
|
|
178
178
|
await checkForSelfUpdate();
|
|
179
179
|
|
|
@@ -219,7 +219,7 @@ mainCli
|
|
|
219
219
|
"Path to Android build tools which contains AAPT2"
|
|
220
220
|
)
|
|
221
221
|
.action(async ({ keypair, buildToolsPath }) => {
|
|
222
|
-
tryWithErrorMessage(async () => {
|
|
222
|
+
await tryWithErrorMessage(async () => {
|
|
223
223
|
latestReleaseMessage();
|
|
224
224
|
await checkForSelfUpdate();
|
|
225
225
|
|
|
@@ -234,8 +234,6 @@ mainCli
|
|
|
234
234
|
signer,
|
|
235
235
|
buildToolsPath: resolvedBuildToolsPath,
|
|
236
236
|
});
|
|
237
|
-
|
|
238
|
-
//TODO: Add pretty formatting here, but will require more work than other sections
|
|
239
237
|
}
|
|
240
238
|
});
|
|
241
239
|
});
|
|
@@ -284,7 +282,7 @@ publishCommand
|
|
|
284
282
|
requestorIsAuthorized,
|
|
285
283
|
dryRun,
|
|
286
284
|
}) => {
|
|
287
|
-
tryWithErrorMessage(async () => {
|
|
285
|
+
await tryWithErrorMessage(async () => {
|
|
288
286
|
await checkForSelfUpdate();
|
|
289
287
|
await checkSubmissionNetwork(url);
|
|
290
288
|
|
|
@@ -355,7 +353,7 @@ publishCommand
|
|
|
355
353
|
critical,
|
|
356
354
|
dryRun,
|
|
357
355
|
}) => {
|
|
358
|
-
tryWithErrorMessage(async () => {
|
|
356
|
+
await tryWithErrorMessage(async () => {
|
|
359
357
|
await checkForSelfUpdate();
|
|
360
358
|
await checkSubmissionNetwork(url);
|
|
361
359
|
|
|
@@ -422,7 +420,7 @@ publishCommand
|
|
|
422
420
|
critical,
|
|
423
421
|
dryRun,
|
|
424
422
|
}) => {
|
|
425
|
-
tryWithErrorMessage(async () => {
|
|
423
|
+
await tryWithErrorMessage(async () => {
|
|
426
424
|
await checkForSelfUpdate();
|
|
427
425
|
await checkSubmissionNetwork(url);
|
|
428
426
|
|
|
@@ -482,7 +480,7 @@ publishCommand
|
|
|
482
480
|
requestDetails,
|
|
483
481
|
{ appMintAddress, releaseMintAddress, keypair, url, requestorIsAuthorized, dryRun }
|
|
484
482
|
) => {
|
|
485
|
-
tryWithErrorMessage(async () => {
|
|
483
|
+
await tryWithErrorMessage(async () => {
|
|
486
484
|
await checkForSelfUpdate();
|
|
487
485
|
await checkSubmissionNetwork(url);
|
|
488
486
|
|
package/src/CliUtils.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { awsStorage } from "@metaplex-foundation/js-plugin-aws";
|
|
|
18
18
|
import { S3StorageManager } from "./config/index.js";
|
|
19
19
|
|
|
20
20
|
export class Constants {
|
|
21
|
-
static CLI_VERSION = "0.
|
|
21
|
+
static CLI_VERSION = "0.5.1";
|
|
22
22
|
static CONFIG_FILE_NAME = "config.yaml";
|
|
23
23
|
static DEFAULT_RPC_DEVNET = "https://api.devnet.solana.com";
|
|
24
24
|
|
|
@@ -78,7 +78,7 @@ export const createPublisherCommand = async ({
|
|
|
78
78
|
);
|
|
79
79
|
|
|
80
80
|
// TODO(sdlaver): dry-run should not modify config
|
|
81
|
-
writeToPublishDetails({ publisher: { address: publisherAddress } });
|
|
81
|
+
await writeToPublishDetails({ publisher: { address: publisherAddress } });
|
|
82
82
|
|
|
83
83
|
return { publisherAddress };
|
|
84
84
|
};
|
|
@@ -99,9 +99,7 @@ export const createReleaseCommand = async ({
|
|
|
99
99
|
storageParams: storageParams,
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
writeToPublishDetails({
|
|
103
|
-
release: { address: releaseAddress },
|
|
104
|
-
});
|
|
102
|
+
await writeToPublishDetails({ release: { address: releaseAddress }, });
|
|
105
103
|
|
|
106
104
|
return { releaseAddress };
|
|
107
105
|
}
|
|
@@ -237,6 +237,7 @@ const getAndroidDetails = async (
|
|
|
237
237
|
min_sdk: parseInt(minSdk?.[1] ?? "0", 10),
|
|
238
238
|
version_code: parseInt(versionCode?.[1] ?? "0", 10),
|
|
239
239
|
version: versionName?.[1] ?? "0",
|
|
240
|
+
cert_fingerprint: await extractCertFingerprint(aaptDir, apkPath),
|
|
240
241
|
permissions: permissions.flatMap(permission => permission[1]),
|
|
241
242
|
locales: localeArray
|
|
242
243
|
};
|
|
@@ -245,6 +246,19 @@ const getAndroidDetails = async (
|
|
|
245
246
|
}
|
|
246
247
|
};
|
|
247
248
|
|
|
249
|
+
export const extractCertFingerprint = async (aaptDir: string, apkPath: string): Promise<string> => {
|
|
250
|
+
const { stdout } = await runExec(`${aaptDir}/apksigner verify --print-certs -v "${apkPath}"`);
|
|
251
|
+
|
|
252
|
+
const regex = /Signer #1 certificate SHA-256 digest:\s*([a-fA-F0-9]+)/;
|
|
253
|
+
const match = stdout.match(regex);
|
|
254
|
+
|
|
255
|
+
if (match && match[1]) {
|
|
256
|
+
return match[1];
|
|
257
|
+
} else {
|
|
258
|
+
throw new Error("Could not obtain cert fingerprint")
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
248
262
|
export const writeToPublishDetails = async ({ publisher, app, release }: SaveToConfigArgs) => {
|
|
249
263
|
const currentConfig = await loadPublishDetailsWithChecks();
|
|
250
264
|
|
|
@@ -267,5 +281,7 @@ export const writeToPublishDetails = async ({ publisher, app, release }: SaveToC
|
|
|
267
281
|
solana_mobile_dapp_publisher_portal: currentConfig.solana_mobile_dapp_publisher_portal
|
|
268
282
|
};
|
|
269
283
|
|
|
270
|
-
fs.writeFileSync(Constants.getConfigFilePath(), dump(newConfig
|
|
284
|
+
fs.writeFileSync(Constants.getConfigFilePath(), dump(newConfig, {
|
|
285
|
+
lineWidth: -1
|
|
286
|
+
}));
|
|
271
287
|
};
|