@blocklet/store 1.16.26-beta-c724c8e6

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/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2018-2020 ArcBlock
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/dist/index.cjs ADDED
@@ -0,0 +1,365 @@
1
+ 'use strict';
2
+
3
+ const security = require('@abtnode/util/lib/security');
4
+ const lodash = require('lodash');
5
+ const tweetnacl = require('tweetnacl');
6
+ const SealedBox = require('tweetnacl-sealedbox-js');
7
+ const joinUrl = require('url-join');
8
+ const axios = require('@abtnode/util/lib/axios');
9
+ const pWaitFor = require('p-wait-for');
10
+ const wallet = require('@ocap/wallet');
11
+ const util$1 = require('@blocklet/meta/lib/util');
12
+ const path = require('path');
13
+ const fs$1 = require('fs-extra');
14
+ const did = require('@arcblock/did');
15
+ const v2 = require('@blocklet/meta/lib/payment/v2');
16
+ const FormData = require('form-data');
17
+ const Client = require('@ocap/client');
18
+ const constant$1 = require('@abtnode/constant');
19
+ const constant = require('@blocklet/constant');
20
+ const util = require('@ocap/util');
21
+ const stableStringify = require('json-stable-stringify');
22
+ const xbytes = require('xbytes');
23
+ const fs = require('fs');
24
+
25
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
26
+
27
+ const tweetnacl__default = /*#__PURE__*/_interopDefaultCompat(tweetnacl);
28
+ const SealedBox__default = /*#__PURE__*/_interopDefaultCompat(SealedBox);
29
+ const joinUrl__default = /*#__PURE__*/_interopDefaultCompat(joinUrl);
30
+ const axios__default = /*#__PURE__*/_interopDefaultCompat(axios);
31
+ const pWaitFor__default = /*#__PURE__*/_interopDefaultCompat(pWaitFor);
32
+ const path__default = /*#__PURE__*/_interopDefaultCompat(path);
33
+ const fs__default$1 = /*#__PURE__*/_interopDefaultCompat(fs$1);
34
+ const FormData__default = /*#__PURE__*/_interopDefaultCompat(FormData);
35
+ const Client__default = /*#__PURE__*/_interopDefaultCompat(Client);
36
+ const stableStringify__default = /*#__PURE__*/_interopDefaultCompat(stableStringify);
37
+ const xbytes__default = /*#__PURE__*/_interopDefaultCompat(xbytes);
38
+ const fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
39
+
40
+ function request(options) {
41
+ return axios__default({
42
+ maxContentLength: Number.POSITIVE_INFINITY,
43
+ maxBodyLength: Number.POSITIVE_INFINITY,
44
+ ...options
45
+ });
46
+ }
47
+
48
+ const fetchConfigs = async ({
49
+ connectUrl,
50
+ sessionId,
51
+ fetchInterval,
52
+ fetchTimeout,
53
+ connectAction
54
+ }) => {
55
+ const ENDPOINT_CHECK_SESSION = `/api/did/${connectAction}/status`;
56
+ const ENDPOINT_INVALIDATE_SESSION = `/api/did/${connectAction}/timeout`;
57
+ const fetchSessionStatus = async () => {
58
+ const url = `${joinUrl__default(connectUrl, ENDPOINT_CHECK_SESSION)}?_t_=${sessionId}`;
59
+ const { data } = await request({
60
+ url,
61
+ method: "GET",
62
+ timeout: fetchTimeout
63
+ });
64
+ return data;
65
+ };
66
+ const condition = async () => {
67
+ const { status, error } = await fetchSessionStatus();
68
+ if (status === "error" || !!error) {
69
+ throw new Error(error);
70
+ }
71
+ return status === "succeed";
72
+ };
73
+ try {
74
+ await pWaitFor__default(condition, { interval: fetchInterval, timeout: fetchTimeout });
75
+ const { config } = await fetchSessionStatus();
76
+ return config;
77
+ } catch (e) {
78
+ const err = e;
79
+ if (err.name === "TimeoutError") {
80
+ await request({
81
+ url: `${joinUrl__default(connectUrl, ENDPOINT_INVALIDATE_SESSION)}?_t_=${sessionId}`,
82
+ method: "GET",
83
+ timeout: fetchTimeout
84
+ });
85
+ }
86
+ throw err;
87
+ }
88
+ };
89
+
90
+ function baseWrapSpinner(_, waiting) {
91
+ return Promise.resolve(waiting());
92
+ }
93
+
94
+ async function createConnect({
95
+ connectUrl = "https://store.blocklet.dev",
96
+ openPage,
97
+ fetchTimeout = 30 * 1e3,
98
+ fetchInterval = 3 * 1e3,
99
+ retry = 60,
100
+ connectAction = "connect-cli",
101
+ wrapSpinner = baseWrapSpinner,
102
+ enableEncrypt = false,
103
+ ...restParams
104
+ } = {}) {
105
+ const ENDPOINT_CREATE_SESSION = `/api/did/${connectAction}/token`;
106
+ const DID_CONNECT_URL = `/${connectAction}`;
107
+ const keyPair = tweetnacl__default.box.keyPair();
108
+ const decrypt = (value) => {
109
+ const decrypted = SealedBox__default.open(
110
+ Uint8Array.from(Buffer.from(value, "base64")),
111
+ keyPair.publicKey,
112
+ keyPair.secretKey
113
+ );
114
+ return JSON.parse(Buffer.from(decrypted).toString("utf8"));
115
+ };
116
+ const BLOCKLET_JSON_PATH = "__blocklet__.js?type=json";
117
+ let masterSite;
118
+ try {
119
+ const { data: blocklet } = await request({
120
+ url: joinUrl__default(connectUrl, BLOCKLET_JSON_PATH),
121
+ method: "GET",
122
+ timeout: fetchTimeout
123
+ });
124
+ masterSite = blocklet?.settings?.federated?.master;
125
+ } catch {
126
+ }
127
+ try {
128
+ const mergeParams = {
129
+ _ek_: security.encodeEncryptionKey(keyPair.publicKey),
130
+ sourceAppPid: masterSite?.appPid || void 0,
131
+ ...restParams
132
+ };
133
+ const res = await request({
134
+ url: joinUrl__default(connectUrl, ENDPOINT_CREATE_SESSION),
135
+ params: mergeParams,
136
+ // for sensitive info encryption
137
+ method: "GET",
138
+ timeout: fetchTimeout
139
+ });
140
+ const { url, token } = res.data;
141
+ const encodedUrl = encodeURIComponent(Buffer.from(url).toString("base64"));
142
+ const pageUrl = `${joinUrl__default(connectUrl, DID_CONNECT_URL)}?__connect_url__=${encodedUrl}`;
143
+ openPage?.(pageUrl);
144
+ return await wrapSpinner(`Waiting for connection: ${connectUrl}`, async () => {
145
+ const fetchData = await fetchConfigs({
146
+ connectUrl,
147
+ sessionId: token,
148
+ connectAction,
149
+ fetchTimeout: retry * fetchInterval,
150
+ fetchInterval: retry
151
+ });
152
+ const decryptData = enableEncrypt ? decrypt(fetchData) : fetchData;
153
+ return decryptData;
154
+ });
155
+ } catch (e) {
156
+ const err = e;
157
+ const response = lodash.get(err, "response");
158
+ let errorMessage;
159
+ if (response) {
160
+ errorMessage = lodash.get(response, "data.error", response.statusText) || "";
161
+ errorMessage = `[${response.status}] ${errorMessage}`;
162
+ } else {
163
+ errorMessage = err.message;
164
+ }
165
+ throw new Error(`failed to connect to store (${errorMessage})`);
166
+ }
167
+ }
168
+
169
+ const getStoreInfo = async (url) => {
170
+ const { data } = await axios__default.get(joinUrl__default(url, constant$1.BLOCKLET_STORE_META_PATH), { timeout: 20 * 1e3 });
171
+ return data;
172
+ };
173
+ const isValidChainEndpoint = async (endpoint) => {
174
+ if (!endpoint) {
175
+ return false;
176
+ }
177
+ const client = new Client__default(endpoint);
178
+ try {
179
+ const { info } = await client.getChainInfo();
180
+ if (info.consensusVersion.split(" ").length === 2) {
181
+ return client;
182
+ }
183
+ return false;
184
+ } catch (err) {
185
+ return false;
186
+ }
187
+ };
188
+ const getShare = async ({
189
+ meta,
190
+ storeUrl,
191
+ developerDid
192
+ }) => {
193
+ const info = await getStoreInfo(storeUrl);
194
+ const shares = lodash.cloneDeep(lodash.get(meta, "payment.share", []));
195
+ if (shares.length === 0) {
196
+ shares.push({
197
+ name: "developer",
198
+ address: developerDid,
199
+ value: constant.BLOCKLET_FACTORY_SHARES.developer
200
+ });
201
+ }
202
+ if (!shares.find((x) => x.address === info.id)) {
203
+ shares.push({
204
+ name: "store",
205
+ address: info.id,
206
+ value: constant.BLOCKLET_FACTORY_SHARES.store
207
+ });
208
+ }
209
+ return shares;
210
+ };
211
+ const ensureBlockletNftFactory = async ({
212
+ meta,
213
+ storeUrl
214
+ }) => {
215
+ const info = await getStoreInfo(storeUrl);
216
+ const endpoint = info.chainHost;
217
+ const ocapClient = await isValidChainEndpoint(endpoint);
218
+ if (ocapClient === false) {
219
+ throw new Error("Invalid chain endpoint fetched from store, please contact store administrator to fix");
220
+ }
221
+ const { itx } = await v2.createNftFactoryItx({
222
+ blockletMeta: meta,
223
+ ocapClient,
224
+ issuers: [info.id],
225
+ storeUrl
226
+ });
227
+ return itx.address;
228
+ };
229
+
230
+ const sign = (blockletMeta, wallet) => {
231
+ const walletJSON = wallet.toJSON();
232
+ const signatureData = {
233
+ type: walletJSON.type.pk,
234
+ name: blockletMeta.name,
235
+ signer: walletJSON.address,
236
+ pk: util.toBase58(walletJSON.pk),
237
+ created: (/* @__PURE__ */ new Date()).toISOString()
238
+ };
239
+ const signature = wallet.sign(stableStringify__default({ ...blockletMeta, signatures: [signatureData] }));
240
+ signatureData.sig = util.toBase58(signature);
241
+ return signatureData;
242
+ };
243
+
244
+ const verifyBundleSize = async ({
245
+ storeUrl,
246
+ tarballFilePath
247
+ }) => {
248
+ const { data: storeJson } = await axios__default({
249
+ url: `${storeUrl}/api/store.json`
250
+ }).catch(() => {
251
+ return {};
252
+ });
253
+ if (xbytes__default.isBytes(storeJson?.maxBundleSize) && fs__default.existsSync(tarballFilePath)) {
254
+ const { size: tarballFileBytes } = fs__default.statSync(tarballFilePath);
255
+ const maxBundleBytes = xbytes__default.parseSize(storeJson.maxBundleSize);
256
+ if (tarballFileBytes > maxBundleBytes) {
257
+ throw new Error(
258
+ `The release file ${path__default.basename(tarballFilePath)} size cannot exceed ${storeJson.maxBundleSize}`
259
+ );
260
+ }
261
+ }
262
+ };
263
+
264
+ async function upload({
265
+ storeUrl,
266
+ accessToken,
267
+ developerDid,
268
+ metaFile,
269
+ wrapSpinner = baseWrapSpinner,
270
+ printSuccess = () => {
271
+ },
272
+ printTar = async () => {
273
+ },
274
+ debug = () => {
275
+ }
276
+ }) {
277
+ if (!fs__default$1.existsSync(metaFile)) {
278
+ throw new Error(`Invalid release meta file ${metaFile} not exists`);
279
+ }
280
+ const meta = fs__default$1.readJSONSync(metaFile);
281
+ if (meta.payment === void 0) {
282
+ const { charging } = meta;
283
+ const payment = {};
284
+ if (charging && Array.isArray(charging.tokens)) {
285
+ payment.price = charging.tokens.map((x) => ({
286
+ address: x.address,
287
+ value: x.price
288
+ }));
289
+ }
290
+ if (charging && Array.isArray(charging.shares)) {
291
+ payment.share = charging.shares.map((x) => ({
292
+ name: x.name,
293
+ address: x.address,
294
+ value: x.share
295
+ }));
296
+ }
297
+ meta.payment = payment;
298
+ delete meta.charging;
299
+ }
300
+ const { tarball } = meta.dist;
301
+ const tarballFilePath = path__default.join(path__default.dirname(metaFile), tarball);
302
+ await verifyBundleSize({ storeUrl, tarballFilePath });
303
+ const wallet$1 = wallet.fromSecretKey(accessToken);
304
+ if (!wallet$1) {
305
+ throw new Error(
306
+ `[NO-ACCESS] Blocklet store accessToken is used to authorize developers when uploading blocklets, you can generate your own accessToken from ${storeUrl}`
307
+ );
308
+ }
309
+ if (util$1.isFreeBlocklet(meta) === false) {
310
+ if (!developerDid || did.isValid(developerDid) === false) {
311
+ throw new Error(
312
+ `developerDid is required to upload a paid blocklet, please get your developerDid from ${storeUrl}`
313
+ );
314
+ }
315
+ try {
316
+ const typeInfo = did.toTypeInfo(meta.did);
317
+ const isNewDid = typeInfo.role === did.types.RoleType.ROLE_BLOCKLET;
318
+ meta.payment.share = await getShare({ meta, storeUrl, developerDid: isNewDid ? meta.did : developerDid });
319
+ const nftFactory = await ensureBlockletNftFactory({ meta, storeUrl });
320
+ meta.nftFactory = nftFactory;
321
+ printSuccess(`NFT Factory for ${meta.name}: ${nftFactory}`);
322
+ } catch (err) {
323
+ throw new Error(`Can not determine NFT factory for ${meta.name}: ${err.message}`);
324
+ }
325
+ } else {
326
+ await v2.checkFreeBlocklet(meta);
327
+ }
328
+ const signature = sign(meta, wallet$1);
329
+ meta.signatures = [signature];
330
+ printSuccess("Blocklet release signed successfully, signature:", signature.sig);
331
+ await printTar(meta, tarballFilePath);
332
+ const data = new FormData__default();
333
+ data.append("blocklet-meta", Buffer.from(JSON.stringify(meta)), { filename: path__default.basename(metaFile) });
334
+ data.append("blocklet-tarball", fs__default$1.readFileSync(tarballFilePath), { filename: path__default.basename(tarballFilePath) });
335
+ let uploadResult = {};
336
+ try {
337
+ await wrapSpinner(`Uploading ${meta.name}@${meta.version}...`, async () => {
338
+ uploadResult = await axios__default({
339
+ url: joinUrl__default(storeUrl, "/api/blocklets/upload"),
340
+ method: "POST",
341
+ data,
342
+ headers: {
343
+ ...data.getHeaders(),
344
+ "blocklet-version": meta.version,
345
+ "blocklet-did": meta.did
346
+ },
347
+ timeout: 10 * 60 * 1e3,
348
+ // 10 minute
349
+ maxContentLength: Number.POSITIVE_INFINITY,
350
+ maxBodyLength: Number.POSITIVE_INFINITY
351
+ });
352
+ });
353
+ debug("Upload result:", uploadResult.data);
354
+ } catch (err) {
355
+ if (err.response) {
356
+ const errorMessage = lodash.get(err.response, "data.error", err.response.statusText);
357
+ throw new Error(`Upload failed with error: [${err.response.status}] ${errorMessage}`);
358
+ }
359
+ throw new Error(`Upload failed with error: ${err.message}`);
360
+ }
361
+ return { ...meta, status: uploadResult.data?.status || "" };
362
+ }
363
+
364
+ exports.createConnect = createConnect;
365
+ exports.upload = upload;
@@ -0,0 +1,34 @@
1
+ declare function baseWrapSpinner(_: string, waiting: () => Promise<unknown>): Promise<unknown>;
2
+
3
+ interface CreateConnectOptions {
4
+ connectUrl?: string;
5
+ openPage?: (url: string) => void;
6
+ fetchTimeout?: number;
7
+ fetchInterval?: number;
8
+ retry?: number;
9
+ connectAction?: string;
10
+ enableEncrypt?: boolean;
11
+ wrapSpinner?: typeof baseWrapSpinner;
12
+ }
13
+ declare function createConnect({ connectUrl, openPage, fetchTimeout, fetchInterval, retry, connectAction, wrapSpinner, enableEncrypt, ...restParams }?: CreateConnectOptions): Promise<unknown>;
14
+
15
+ interface BlockletMeta {
16
+ name: string;
17
+ version: string;
18
+ did: string;
19
+ status: string;
20
+ }
21
+
22
+ interface UploadOptions {
23
+ storeUrl: string;
24
+ accessToken: string;
25
+ developerDid: string;
26
+ metaFile: string;
27
+ wrapSpinner?: typeof baseWrapSpinner;
28
+ printSuccess: (...args: unknown[]) => void;
29
+ printTar: (meta: unknown, metaPath: string) => Promise<void>;
30
+ debug: (...args: unknown[]) => void;
31
+ }
32
+ declare function upload({ storeUrl, accessToken, developerDid, metaFile, wrapSpinner, printSuccess, printTar, debug, }: UploadOptions): Promise<BlockletMeta | undefined>;
33
+
34
+ export { type UploadOptions, createConnect, upload };
@@ -0,0 +1,34 @@
1
+ declare function baseWrapSpinner(_: string, waiting: () => Promise<unknown>): Promise<unknown>;
2
+
3
+ interface CreateConnectOptions {
4
+ connectUrl?: string;
5
+ openPage?: (url: string) => void;
6
+ fetchTimeout?: number;
7
+ fetchInterval?: number;
8
+ retry?: number;
9
+ connectAction?: string;
10
+ enableEncrypt?: boolean;
11
+ wrapSpinner?: typeof baseWrapSpinner;
12
+ }
13
+ declare function createConnect({ connectUrl, openPage, fetchTimeout, fetchInterval, retry, connectAction, wrapSpinner, enableEncrypt, ...restParams }?: CreateConnectOptions): Promise<unknown>;
14
+
15
+ interface BlockletMeta {
16
+ name: string;
17
+ version: string;
18
+ did: string;
19
+ status: string;
20
+ }
21
+
22
+ interface UploadOptions {
23
+ storeUrl: string;
24
+ accessToken: string;
25
+ developerDid: string;
26
+ metaFile: string;
27
+ wrapSpinner?: typeof baseWrapSpinner;
28
+ printSuccess: (...args: unknown[]) => void;
29
+ printTar: (meta: unknown, metaPath: string) => Promise<void>;
30
+ debug: (...args: unknown[]) => void;
31
+ }
32
+ declare function upload({ storeUrl, accessToken, developerDid, metaFile, wrapSpinner, printSuccess, printTar, debug, }: UploadOptions): Promise<BlockletMeta | undefined>;
33
+
34
+ export { type UploadOptions, createConnect, upload };
@@ -0,0 +1,34 @@
1
+ declare function baseWrapSpinner(_: string, waiting: () => Promise<unknown>): Promise<unknown>;
2
+
3
+ interface CreateConnectOptions {
4
+ connectUrl?: string;
5
+ openPage?: (url: string) => void;
6
+ fetchTimeout?: number;
7
+ fetchInterval?: number;
8
+ retry?: number;
9
+ connectAction?: string;
10
+ enableEncrypt?: boolean;
11
+ wrapSpinner?: typeof baseWrapSpinner;
12
+ }
13
+ declare function createConnect({ connectUrl, openPage, fetchTimeout, fetchInterval, retry, connectAction, wrapSpinner, enableEncrypt, ...restParams }?: CreateConnectOptions): Promise<unknown>;
14
+
15
+ interface BlockletMeta {
16
+ name: string;
17
+ version: string;
18
+ did: string;
19
+ status: string;
20
+ }
21
+
22
+ interface UploadOptions {
23
+ storeUrl: string;
24
+ accessToken: string;
25
+ developerDid: string;
26
+ metaFile: string;
27
+ wrapSpinner?: typeof baseWrapSpinner;
28
+ printSuccess: (...args: unknown[]) => void;
29
+ printTar: (meta: unknown, metaPath: string) => Promise<void>;
30
+ debug: (...args: unknown[]) => void;
31
+ }
32
+ declare function upload({ storeUrl, accessToken, developerDid, metaFile, wrapSpinner, printSuccess, printTar, debug, }: UploadOptions): Promise<BlockletMeta | undefined>;
33
+
34
+ export { type UploadOptions, createConnect, upload };
package/dist/index.mjs ADDED
@@ -0,0 +1,347 @@
1
+ import { encodeEncryptionKey } from '@abtnode/util/lib/security';
2
+ import { get, cloneDeep } from 'lodash';
3
+ import tweetnacl from 'tweetnacl';
4
+ import SealedBox from 'tweetnacl-sealedbox-js';
5
+ import joinUrl from 'url-join';
6
+ import axios from '@abtnode/util/lib/axios';
7
+ import pWaitFor from 'p-wait-for';
8
+ import { fromSecretKey } from '@ocap/wallet';
9
+ import { isFreeBlocklet } from '@blocklet/meta/lib/util';
10
+ import path from 'path';
11
+ import fs$1 from 'fs-extra';
12
+ import { isValid, toTypeInfo, types } from '@arcblock/did';
13
+ import { createNftFactoryItx, checkFreeBlocklet } from '@blocklet/meta/lib/payment/v2';
14
+ import FormData from 'form-data';
15
+ import Client from '@ocap/client';
16
+ import { BLOCKLET_STORE_META_PATH } from '@abtnode/constant';
17
+ import { BLOCKLET_FACTORY_SHARES } from '@blocklet/constant';
18
+ import { toBase58 } from '@ocap/util';
19
+ import stableStringify from 'json-stable-stringify';
20
+ import xbytes from 'xbytes';
21
+ import fs from 'fs';
22
+
23
+ function request(options) {
24
+ return axios({
25
+ maxContentLength: Number.POSITIVE_INFINITY,
26
+ maxBodyLength: Number.POSITIVE_INFINITY,
27
+ ...options
28
+ });
29
+ }
30
+
31
+ const fetchConfigs = async ({
32
+ connectUrl,
33
+ sessionId,
34
+ fetchInterval,
35
+ fetchTimeout,
36
+ connectAction
37
+ }) => {
38
+ const ENDPOINT_CHECK_SESSION = `/api/did/${connectAction}/status`;
39
+ const ENDPOINT_INVALIDATE_SESSION = `/api/did/${connectAction}/timeout`;
40
+ const fetchSessionStatus = async () => {
41
+ const url = `${joinUrl(connectUrl, ENDPOINT_CHECK_SESSION)}?_t_=${sessionId}`;
42
+ const { data } = await request({
43
+ url,
44
+ method: "GET",
45
+ timeout: fetchTimeout
46
+ });
47
+ return data;
48
+ };
49
+ const condition = async () => {
50
+ const { status, error } = await fetchSessionStatus();
51
+ if (status === "error" || !!error) {
52
+ throw new Error(error);
53
+ }
54
+ return status === "succeed";
55
+ };
56
+ try {
57
+ await pWaitFor(condition, { interval: fetchInterval, timeout: fetchTimeout });
58
+ const { config } = await fetchSessionStatus();
59
+ return config;
60
+ } catch (e) {
61
+ const err = e;
62
+ if (err.name === "TimeoutError") {
63
+ await request({
64
+ url: `${joinUrl(connectUrl, ENDPOINT_INVALIDATE_SESSION)}?_t_=${sessionId}`,
65
+ method: "GET",
66
+ timeout: fetchTimeout
67
+ });
68
+ }
69
+ throw err;
70
+ }
71
+ };
72
+
73
+ function baseWrapSpinner(_, waiting) {
74
+ return Promise.resolve(waiting());
75
+ }
76
+
77
+ async function createConnect({
78
+ connectUrl = "https://store.blocklet.dev",
79
+ openPage,
80
+ fetchTimeout = 30 * 1e3,
81
+ fetchInterval = 3 * 1e3,
82
+ retry = 60,
83
+ connectAction = "connect-cli",
84
+ wrapSpinner = baseWrapSpinner,
85
+ enableEncrypt = false,
86
+ ...restParams
87
+ } = {}) {
88
+ const ENDPOINT_CREATE_SESSION = `/api/did/${connectAction}/token`;
89
+ const DID_CONNECT_URL = `/${connectAction}`;
90
+ const keyPair = tweetnacl.box.keyPair();
91
+ const decrypt = (value) => {
92
+ const decrypted = SealedBox.open(
93
+ Uint8Array.from(Buffer.from(value, "base64")),
94
+ keyPair.publicKey,
95
+ keyPair.secretKey
96
+ );
97
+ return JSON.parse(Buffer.from(decrypted).toString("utf8"));
98
+ };
99
+ const BLOCKLET_JSON_PATH = "__blocklet__.js?type=json";
100
+ let masterSite;
101
+ try {
102
+ const { data: blocklet } = await request({
103
+ url: joinUrl(connectUrl, BLOCKLET_JSON_PATH),
104
+ method: "GET",
105
+ timeout: fetchTimeout
106
+ });
107
+ masterSite = blocklet?.settings?.federated?.master;
108
+ } catch {
109
+ }
110
+ try {
111
+ const mergeParams = {
112
+ _ek_: encodeEncryptionKey(keyPair.publicKey),
113
+ sourceAppPid: masterSite?.appPid || void 0,
114
+ ...restParams
115
+ };
116
+ const res = await request({
117
+ url: joinUrl(connectUrl, ENDPOINT_CREATE_SESSION),
118
+ params: mergeParams,
119
+ // for sensitive info encryption
120
+ method: "GET",
121
+ timeout: fetchTimeout
122
+ });
123
+ const { url, token } = res.data;
124
+ const encodedUrl = encodeURIComponent(Buffer.from(url).toString("base64"));
125
+ const pageUrl = `${joinUrl(connectUrl, DID_CONNECT_URL)}?__connect_url__=${encodedUrl}`;
126
+ openPage?.(pageUrl);
127
+ return await wrapSpinner(`Waiting for connection: ${connectUrl}`, async () => {
128
+ const fetchData = await fetchConfigs({
129
+ connectUrl,
130
+ sessionId: token,
131
+ connectAction,
132
+ fetchTimeout: retry * fetchInterval,
133
+ fetchInterval: retry
134
+ });
135
+ const decryptData = enableEncrypt ? decrypt(fetchData) : fetchData;
136
+ return decryptData;
137
+ });
138
+ } catch (e) {
139
+ const err = e;
140
+ const response = get(err, "response");
141
+ let errorMessage;
142
+ if (response) {
143
+ errorMessage = get(response, "data.error", response.statusText) || "";
144
+ errorMessage = `[${response.status}] ${errorMessage}`;
145
+ } else {
146
+ errorMessage = err.message;
147
+ }
148
+ throw new Error(`failed to connect to store (${errorMessage})`);
149
+ }
150
+ }
151
+
152
+ const getStoreInfo = async (url) => {
153
+ const { data } = await axios.get(joinUrl(url, BLOCKLET_STORE_META_PATH), { timeout: 20 * 1e3 });
154
+ return data;
155
+ };
156
+ const isValidChainEndpoint = async (endpoint) => {
157
+ if (!endpoint) {
158
+ return false;
159
+ }
160
+ const client = new Client(endpoint);
161
+ try {
162
+ const { info } = await client.getChainInfo();
163
+ if (info.consensusVersion.split(" ").length === 2) {
164
+ return client;
165
+ }
166
+ return false;
167
+ } catch (err) {
168
+ return false;
169
+ }
170
+ };
171
+ const getShare = async ({
172
+ meta,
173
+ storeUrl,
174
+ developerDid
175
+ }) => {
176
+ const info = await getStoreInfo(storeUrl);
177
+ const shares = cloneDeep(get(meta, "payment.share", []));
178
+ if (shares.length === 0) {
179
+ shares.push({
180
+ name: "developer",
181
+ address: developerDid,
182
+ value: BLOCKLET_FACTORY_SHARES.developer
183
+ });
184
+ }
185
+ if (!shares.find((x) => x.address === info.id)) {
186
+ shares.push({
187
+ name: "store",
188
+ address: info.id,
189
+ value: BLOCKLET_FACTORY_SHARES.store
190
+ });
191
+ }
192
+ return shares;
193
+ };
194
+ const ensureBlockletNftFactory = async ({
195
+ meta,
196
+ storeUrl
197
+ }) => {
198
+ const info = await getStoreInfo(storeUrl);
199
+ const endpoint = info.chainHost;
200
+ const ocapClient = await isValidChainEndpoint(endpoint);
201
+ if (ocapClient === false) {
202
+ throw new Error("Invalid chain endpoint fetched from store, please contact store administrator to fix");
203
+ }
204
+ const { itx } = await createNftFactoryItx({
205
+ blockletMeta: meta,
206
+ ocapClient,
207
+ issuers: [info.id],
208
+ storeUrl
209
+ });
210
+ return itx.address;
211
+ };
212
+
213
+ const sign = (blockletMeta, wallet) => {
214
+ const walletJSON = wallet.toJSON();
215
+ const signatureData = {
216
+ type: walletJSON.type.pk,
217
+ name: blockletMeta.name,
218
+ signer: walletJSON.address,
219
+ pk: toBase58(walletJSON.pk),
220
+ created: (/* @__PURE__ */ new Date()).toISOString()
221
+ };
222
+ const signature = wallet.sign(stableStringify({ ...blockletMeta, signatures: [signatureData] }));
223
+ signatureData.sig = toBase58(signature);
224
+ return signatureData;
225
+ };
226
+
227
+ const verifyBundleSize = async ({
228
+ storeUrl,
229
+ tarballFilePath
230
+ }) => {
231
+ const { data: storeJson } = await axios({
232
+ url: `${storeUrl}/api/store.json`
233
+ }).catch(() => {
234
+ return {};
235
+ });
236
+ if (xbytes.isBytes(storeJson?.maxBundleSize) && fs.existsSync(tarballFilePath)) {
237
+ const { size: tarballFileBytes } = fs.statSync(tarballFilePath);
238
+ const maxBundleBytes = xbytes.parseSize(storeJson.maxBundleSize);
239
+ if (tarballFileBytes > maxBundleBytes) {
240
+ throw new Error(
241
+ `The release file ${path.basename(tarballFilePath)} size cannot exceed ${storeJson.maxBundleSize}`
242
+ );
243
+ }
244
+ }
245
+ };
246
+
247
+ async function upload({
248
+ storeUrl,
249
+ accessToken,
250
+ developerDid,
251
+ metaFile,
252
+ wrapSpinner = baseWrapSpinner,
253
+ printSuccess = () => {
254
+ },
255
+ printTar = async () => {
256
+ },
257
+ debug = () => {
258
+ }
259
+ }) {
260
+ if (!fs$1.existsSync(metaFile)) {
261
+ throw new Error(`Invalid release meta file ${metaFile} not exists`);
262
+ }
263
+ const meta = fs$1.readJSONSync(metaFile);
264
+ if (meta.payment === void 0) {
265
+ const { charging } = meta;
266
+ const payment = {};
267
+ if (charging && Array.isArray(charging.tokens)) {
268
+ payment.price = charging.tokens.map((x) => ({
269
+ address: x.address,
270
+ value: x.price
271
+ }));
272
+ }
273
+ if (charging && Array.isArray(charging.shares)) {
274
+ payment.share = charging.shares.map((x) => ({
275
+ name: x.name,
276
+ address: x.address,
277
+ value: x.share
278
+ }));
279
+ }
280
+ meta.payment = payment;
281
+ delete meta.charging;
282
+ }
283
+ const { tarball } = meta.dist;
284
+ const tarballFilePath = path.join(path.dirname(metaFile), tarball);
285
+ await verifyBundleSize({ storeUrl, tarballFilePath });
286
+ const wallet = fromSecretKey(accessToken);
287
+ if (!wallet) {
288
+ throw new Error(
289
+ `[NO-ACCESS] Blocklet store accessToken is used to authorize developers when uploading blocklets, you can generate your own accessToken from ${storeUrl}`
290
+ );
291
+ }
292
+ if (isFreeBlocklet(meta) === false) {
293
+ if (!developerDid || isValid(developerDid) === false) {
294
+ throw new Error(
295
+ `developerDid is required to upload a paid blocklet, please get your developerDid from ${storeUrl}`
296
+ );
297
+ }
298
+ try {
299
+ const typeInfo = toTypeInfo(meta.did);
300
+ const isNewDid = typeInfo.role === types.RoleType.ROLE_BLOCKLET;
301
+ meta.payment.share = await getShare({ meta, storeUrl, developerDid: isNewDid ? meta.did : developerDid });
302
+ const nftFactory = await ensureBlockletNftFactory({ meta, storeUrl });
303
+ meta.nftFactory = nftFactory;
304
+ printSuccess(`NFT Factory for ${meta.name}: ${nftFactory}`);
305
+ } catch (err) {
306
+ throw new Error(`Can not determine NFT factory for ${meta.name}: ${err.message}`);
307
+ }
308
+ } else {
309
+ await checkFreeBlocklet(meta);
310
+ }
311
+ const signature = sign(meta, wallet);
312
+ meta.signatures = [signature];
313
+ printSuccess("Blocklet release signed successfully, signature:", signature.sig);
314
+ await printTar(meta, tarballFilePath);
315
+ const data = new FormData();
316
+ data.append("blocklet-meta", Buffer.from(JSON.stringify(meta)), { filename: path.basename(metaFile) });
317
+ data.append("blocklet-tarball", fs$1.readFileSync(tarballFilePath), { filename: path.basename(tarballFilePath) });
318
+ let uploadResult = {};
319
+ try {
320
+ await wrapSpinner(`Uploading ${meta.name}@${meta.version}...`, async () => {
321
+ uploadResult = await axios({
322
+ url: joinUrl(storeUrl, "/api/blocklets/upload"),
323
+ method: "POST",
324
+ data,
325
+ headers: {
326
+ ...data.getHeaders(),
327
+ "blocklet-version": meta.version,
328
+ "blocklet-did": meta.did
329
+ },
330
+ timeout: 10 * 60 * 1e3,
331
+ // 10 minute
332
+ maxContentLength: Number.POSITIVE_INFINITY,
333
+ maxBodyLength: Number.POSITIVE_INFINITY
334
+ });
335
+ });
336
+ debug("Upload result:", uploadResult.data);
337
+ } catch (err) {
338
+ if (err.response) {
339
+ const errorMessage = get(err.response, "data.error", err.response.statusText);
340
+ throw new Error(`Upload failed with error: [${err.response.status}] ${errorMessage}`);
341
+ }
342
+ throw new Error(`Upload failed with error: ${err.message}`);
343
+ }
344
+ return { ...meta, status: uploadResult.data?.status || "" };
345
+ }
346
+
347
+ export { createConnect, upload };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@blocklet/store",
3
+ "version": "1.16.26-beta-c724c8e6",
4
+ "description": "Connect Store and upload blocklet to Store",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.cjs"
12
+ }
13
+ },
14
+ "main": "./dist/index.cjs",
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "ts-check": "npx tsc --noemit --skipLibCheck --incremental --tsBuildInfoFile './node_modules/.tsbuildinfo'",
21
+ "lint": "npm run ts-check && eslint src",
22
+ "lint:fix": "npm run lint -- --fix",
23
+ "build": "unbuild",
24
+ "build:blocklet-store": "npm run build"
25
+ },
26
+ "keywords": [],
27
+ "author": "",
28
+ "license": "ISC",
29
+ "dependencies": {
30
+ "@abtnode/constant": "1.16.26-beta-c724c8e6",
31
+ "@abtnode/util": "1.16.26-beta-c724c8e6",
32
+ "@arcblock/did": "1.18.114",
33
+ "@blocklet/constant": "1.16.26-beta-c724c8e6",
34
+ "@blocklet/meta": "1.16.26-beta-c724c8e6",
35
+ "@ocap/client": "^1.18.114",
36
+ "@ocap/util": "1.18.114",
37
+ "@ocap/wallet": "1.18.114",
38
+ "form-data": "^4.0.0",
39
+ "fs-extra": "^11.2.0",
40
+ "json-stable-stringify": "^1.0.1",
41
+ "lodash": "^4.17.21",
42
+ "p-wait-for": "3.2.0",
43
+ "tweetnacl": "^1.0.3",
44
+ "tweetnacl-sealedbox-js": "^1.2.0",
45
+ "url-join": "^4.0.1",
46
+ "xbytes": "^1.8.0"
47
+ },
48
+ "devDependencies": {
49
+ "@arcblock/eslint-config-ts": "^0.3.0",
50
+ "@types/jest": "^29.5.11",
51
+ "@types/node": "^18.11.0",
52
+ "@typescript-eslint/eslint-plugin": "^5.40.1",
53
+ "@typescript-eslint/parser": "^5.40.1",
54
+ "dts-bundle-generator": "^9.2.3",
55
+ "eslint": "^8.25.0",
56
+ "jest": "^29.7.0",
57
+ "prettier": "^2.7.1",
58
+ "ts-jest": "^29.1.1",
59
+ "typescript": "^5.0.4",
60
+ "unbuild": "^2.0.0"
61
+ },
62
+ "gitHead": "f2b51533d934be041b1cc5b4b2fc8c56f4855a26"
63
+ }