@blocklet/launcher-util 2.2.3 → 2.2.5

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/es/constant.js ADDED
@@ -0,0 +1,169 @@
1
+ const INSTANCE_STATUS = Object.freeze({
2
+ error: 0,
3
+ waiting: 5,
4
+ pending: 10,
5
+ starting: 20,
6
+ running: 30,
7
+ restarting: 40,
8
+ stopping: 50,
9
+ stopped: 60,
10
+ terminated: 70,
11
+ expired: 80,
12
+ underMaintenance: 90
13
+ });
14
+ const PLAN_STATUS = Object.freeze({
15
+ draft: 10,
16
+ published: 20,
17
+ dropped: 30
18
+ // 已下架
19
+ });
20
+ const LAUNCH_STATUS = Object.freeze({
21
+ created: 0,
22
+ selected: 10,
23
+ connected: 20,
24
+ paid: 30,
25
+ allocated: 40,
26
+ installed: 50,
27
+ expired: 60,
28
+ terminated: 70,
29
+ timeout: 80,
30
+ transferred: 90
31
+ });
32
+ const LAUNCH_ACTIVITY_TYPE = Object.freeze({
33
+ created: "created",
34
+ selected: "selected",
35
+ connected: "connected",
36
+ paid: "paid",
37
+ allocated: "allocated",
38
+ installed: "installed",
39
+ timeout: "timeout",
40
+ expired: "expired",
41
+ terminated: "terminated",
42
+ transferred: "transferred"
43
+ });
44
+ const SERVER_ACTIVITY_TYPE = Object.freeze({
45
+ purchase: "purchase",
46
+ launch: "launch",
47
+ createFailed: "createFailed",
48
+ created: "created",
49
+ start: "start",
50
+ started: "started",
51
+ startFailed: "startFailed",
52
+ stop: "stop",
53
+ stopped: "stopped",
54
+ stopFailed: "stopFailed",
55
+ restart: "restart",
56
+ restarted: "restarted",
57
+ restartFailed: "restartFailed",
58
+ expired: "expired",
59
+ terminate: "terminate",
60
+ terminated: "terminated",
61
+ terminateFailed: "terminateFailed",
62
+ renewaled: "renewaled",
63
+ transferred: "transferred",
64
+ replacement: "replacement"
65
+ });
66
+ const SERVERLESS_INSTANCE_STATUS = {
67
+ launching: 10,
68
+ running: 20,
69
+ expired: 30,
70
+ terminated: 40
71
+ };
72
+ const toMap = (staus) => Object.keys(staus).reduce((acc, cur) => {
73
+ acc[staus[cur]] = cur;
74
+ return acc;
75
+ }, {});
76
+ const fromStatus = (map) => (status) => map[status];
77
+ const statusInstanceMap = toMap(INSTANCE_STATUS);
78
+ const planStatusMap = toMap(PLAN_STATUS);
79
+ const fromInstanceStatus = fromStatus(statusInstanceMap);
80
+ const fromPlanStatus = fromStatus(planStatusMap);
81
+ const NFT_TYPE_SERVERLESS = "BlockletServerServerlessNFT";
82
+ const BLOCKLET_SERVER_OWNERSHIP_NFT = "BlockletServerOwnershipNFT";
83
+ const TIME_LOCALE = {
84
+ en: {
85
+ h: "hour",
86
+ hs: "hours",
87
+ d: " day",
88
+ ds: " days",
89
+ m: " month",
90
+ ms: " months",
91
+ y: " year",
92
+ ys: " years"
93
+ },
94
+ zh: {
95
+ h: "小时",
96
+ hs: "小时",
97
+ d: "天",
98
+ ds: "天",
99
+ m: "个月",
100
+ ms: "个月",
101
+ y: "年",
102
+ ys: "年"
103
+ }
104
+ };
105
+ const APP_TYPE = Object.freeze({
106
+ serverless: "serverless",
107
+ dedicated: "dedicated"
108
+ });
109
+ const SKU_STATUS = Object.freeze({
110
+ DISABLED: 0,
111
+ ENABLED: 1
112
+ });
113
+ const PAYMENT_STATUS = Object.freeze({
114
+ unpaid: 10,
115
+ paid: 20,
116
+ done: 30,
117
+ expired: 40,
118
+ canceled: 60,
119
+ failed: 70
120
+ });
121
+ const PAYMENT_TYPES = {
122
+ purchase: "purchase",
123
+ renewal: "renewal",
124
+ autoRenewal: "auto-renewal"
125
+ };
126
+ const PAYMENT_METHODS = Object.freeze({
127
+ crypto: "crypto",
128
+ stripe: "stripe",
129
+ fiat: "fiat"
130
+ // TODO: 区分支付货币和支付方式
131
+ });
132
+ const DID_DOMAIN_SUFFIX = "did.abtnet.io";
133
+ const REDEEM_NFT_ID = "redeem";
134
+ const INSTANCE_MAX_NAME_LENGTH = 30;
135
+ const INSTANCE_MAX_DESC_LENGTH = 50;
136
+ const LAUNCH_TYPE = {
137
+ instant: "instant",
138
+ redeem: "redeem"
139
+ };
140
+ const CURRENCY_TYPE = {
141
+ fiat: "fiat",
142
+ crypto: "crypto"
143
+ };
144
+ const SERVERLESS_RETAIN_DAYS = 30;
145
+ export {
146
+ APP_TYPE,
147
+ BLOCKLET_SERVER_OWNERSHIP_NFT,
148
+ CURRENCY_TYPE,
149
+ DID_DOMAIN_SUFFIX,
150
+ INSTANCE_MAX_DESC_LENGTH,
151
+ INSTANCE_MAX_NAME_LENGTH,
152
+ INSTANCE_STATUS,
153
+ LAUNCH_ACTIVITY_TYPE,
154
+ LAUNCH_STATUS,
155
+ LAUNCH_TYPE,
156
+ NFT_TYPE_SERVERLESS,
157
+ PAYMENT_METHODS,
158
+ PAYMENT_STATUS,
159
+ PAYMENT_TYPES,
160
+ PLAN_STATUS,
161
+ REDEEM_NFT_ID,
162
+ SERVERLESS_INSTANCE_STATUS,
163
+ SERVERLESS_RETAIN_DAYS,
164
+ SERVER_ACTIVITY_TYPE,
165
+ SKU_STATUS,
166
+ TIME_LOCALE,
167
+ fromInstanceStatus,
168
+ fromPlanStatus
169
+ };
@@ -0,0 +1,25 @@
1
+ const formatError = (err) => {
2
+ var _a;
3
+ if (!err) {
4
+ return err;
5
+ }
6
+ const { details, errors, response } = err;
7
+ if (Array.isArray(errors)) {
8
+ return errors.map((x) => x.message).join("\n");
9
+ }
10
+ if (Array.isArray(details)) {
11
+ const formatted = details.map((e) => {
12
+ const errorMessage = e.message.replace(/["]/g, "'");
13
+ const errorPath = e.path.join(".");
14
+ return `${errorPath}: ${errorMessage}`;
15
+ });
16
+ return `Validate failed: ${formatted.join(";")}`;
17
+ }
18
+ if (response) {
19
+ return ((_a = response.data) == null ? void 0 : _a.error) || `Request failed: ${response.status} ${response.statusText}: ${JSON.stringify(response.data)}`;
20
+ }
21
+ return err.message || err;
22
+ };
23
+ export {
24
+ formatError as default
25
+ };
@@ -0,0 +1,45 @@
1
+ import joinUrl from "url-join";
2
+ import axios from "axios";
3
+ import get from "lodash.get";
4
+ const getAsset = async (chainHost, address) => {
5
+ const url = joinUrl(new URL(chainHost).origin, "/api/gql/");
6
+ const result = await axios.post(
7
+ url,
8
+ JSON.stringify({
9
+ query: `{
10
+ getAssetState(address: "${address}") {
11
+ state {
12
+ address
13
+ data {
14
+ typeUrl
15
+ value
16
+ }
17
+ display {
18
+ type
19
+ content
20
+ }
21
+ issuer
22
+ owner
23
+ parent
24
+ tags
25
+ }
26
+ }
27
+ }`
28
+ }),
29
+ {
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ Accept: "application/json"
33
+ },
34
+ timeout: 60 * 1e3
35
+ }
36
+ );
37
+ const state = get(result, "data.data.getAssetState.state");
38
+ if (state && state.data.typeUrl === "json") {
39
+ state.data.value = JSON.parse(state.data.value);
40
+ }
41
+ return state;
42
+ };
43
+ export {
44
+ getAsset as default
45
+ };
@@ -0,0 +1,43 @@
1
+ import flat from "flat";
2
+ import { SERVERLESS_INSTANCE_STATUS, INSTANCE_STATUS, PAYMENT_METHODS, CURRENCY_TYPE } from "../constant";
3
+ const en = flat({
4
+ common: {
5
+ app: "App",
6
+ billing: "Billing",
7
+ duration: "Duration",
8
+ spaceName: "Space Name"
9
+ },
10
+ serverlessInstance: {
11
+ appStatus: {
12
+ [SERVERLESS_INSTANCE_STATUS.launching]: "Launching",
13
+ [SERVERLESS_INSTANCE_STATUS.running]: "Running",
14
+ [SERVERLESS_INSTANCE_STATUS.expired]: "Expired",
15
+ [SERVERLESS_INSTANCE_STATUS.terminated]: "Terminated"
16
+ }
17
+ },
18
+ dedicatedInstance: {
19
+ appStatus: {
20
+ [INSTANCE_STATUS.unknown]: "Unknown",
21
+ [INSTANCE_STATUS.pending]: "Pending",
22
+ [INSTANCE_STATUS.starting]: "Starting",
23
+ [INSTANCE_STATUS.running]: "Running",
24
+ [INSTANCE_STATUS.restarting]: "Restarting",
25
+ [INSTANCE_STATUS.stopping]: "Stopping",
26
+ [INSTANCE_STATUS.stopped]: "Stopped",
27
+ [INSTANCE_STATUS.terminated]: "Terminated",
28
+ [INSTANCE_STATUS.expired]: "Expired",
29
+ [INSTANCE_STATUS.error]: "Error",
30
+ [INSTANCE_STATUS.waiting]: "Waiting",
31
+ [INSTANCE_STATUS.underMaintenance]: "Under-Maintenance"
32
+ }
33
+ },
34
+ paymentMethod: {
35
+ [PAYMENT_METHODS.crypto]: "Crypto",
36
+ [PAYMENT_METHODS.stripe]: "Credit",
37
+ [CURRENCY_TYPE.fiat]: "Credit",
38
+ [CURRENCY_TYPE.crypto]: "Crypto"
39
+ }
40
+ });
41
+ export {
42
+ en as default
43
+ };
@@ -0,0 +1,17 @@
1
+ const index = (locales) => {
2
+ const replace = (template, data) => (template || "").replace(
3
+ /{(\w*)}/g,
4
+ (_, key) => Object.prototype.hasOwnProperty.call(data || {}, key) ? data[key] : ""
5
+ );
6
+ const translate = (key, locale, params) => {
7
+ if (!locales[locale]) {
8
+ locale = "en";
9
+ }
10
+ return replace(locales[locale][key], params) || key;
11
+ };
12
+ const createTranslateFunc = (locale) => (key, params) => translate(key, locale || "en", params);
13
+ return { translate, createTranslateFunc };
14
+ };
15
+ export {
16
+ index as default
17
+ };
@@ -0,0 +1,45 @@
1
+ import flat from "flat";
2
+ import { SERVERLESS_INSTANCE_STATUS, INSTANCE_STATUS, PAYMENT_METHODS, CURRENCY_TYPE } from "../constant";
3
+ const zh = flat({
4
+ common: {
5
+ app: "应用",
6
+ billing: "账单",
7
+ duration: "时长",
8
+ spaceName: "应用空间"
9
+ },
10
+ serverlessInstance: {
11
+ appStatus: {
12
+ [SERVERLESS_INSTANCE_STATUS.launching]: "启动中",
13
+ [SERVERLESS_INSTANCE_STATUS.running]: "运行中",
14
+ [SERVERLESS_INSTANCE_STATUS.expired]: "已过期",
15
+ [SERVERLESS_INSTANCE_STATUS.terminated]: "已终止"
16
+ }
17
+ },
18
+ dedicatedInstance: {
19
+ appStatus: {
20
+ [INSTANCE_STATUS.unknown]: "未知",
21
+ [INSTANCE_STATUS.pending]: "待处理",
22
+ [INSTANCE_STATUS.starting]: "启动中",
23
+ [INSTANCE_STATUS.running]: "运行中",
24
+ [INSTANCE_STATUS.restarting]: "重启中",
25
+ [INSTANCE_STATUS.stopping]: "停止中",
26
+ [INSTANCE_STATUS.stopped]: "已停止",
27
+ [INSTANCE_STATUS.terminated]: "已终止",
28
+ [INSTANCE_STATUS.expired]: "已过期",
29
+ [INSTANCE_STATUS.error]: "错误",
30
+ [INSTANCE_STATUS.waiting]: "等待中",
31
+ [INSTANCE_STATUS.underMaintenance]: "维护中"
32
+ }
33
+ },
34
+ paymentMethod: {
35
+ [PAYMENT_METHODS.crypto]: "加密货币",
36
+ [PAYMENT_METHODS.stripe]: "信用卡",
37
+ [CURRENCY_TYPE.fiat]: "信用卡",
38
+ // TODO: 统一管理支付方式和货币类型
39
+ [CURRENCY_TYPE.crypto]: "加密货币"
40
+ // TODO: 统一管理支付方式和货币类型
41
+ }
42
+ });
43
+ export {
44
+ zh as default
45
+ };
@@ -0,0 +1,66 @@
1
+ import pick from "lodash.pick";
2
+ import { getSort } from "./util";
3
+ const MAX_PAGING_SIZE = 100;
4
+ const DEFAULT_PAGING_SIZE = MAX_PAGING_SIZE;
5
+ const DEFAULT_PAGE = 1;
6
+ const pagination = (req, res, next) => {
7
+ const paging = {
8
+ page: DEFAULT_PAGE,
9
+ size: DEFAULT_PAGING_SIZE
10
+ };
11
+ req.paging = paging;
12
+ if (!Number.isNaN(Number(req.query.page))) {
13
+ paging.page = Number(req.query.page);
14
+ }
15
+ if (!Number.isNaN(Number(req.query.size))) {
16
+ const size = Number(req.query.size);
17
+ paging.size = size <= MAX_PAGING_SIZE ? size : MAX_PAGING_SIZE;
18
+ }
19
+ paging.sort = getSort(req.query.sortby, req.query.sortdir);
20
+ next();
21
+ };
22
+ const validate = (schema, option = {}) => (req, res, next) => {
23
+ const { error, value } = schema.validate(req.body, {
24
+ errors: { language: req.query.locale || "en" },
25
+ ...option
26
+ });
27
+ if (error) {
28
+ res.status(400).json({ error: error.message });
29
+ return;
30
+ }
31
+ req.data = value;
32
+ next();
33
+ };
34
+ const createAuditMiddleware = ({ baseURL, writer, formatterMap }) => (req, res, next) => {
35
+ if (["PUT", "POST", "PATCH", "DELETE"].includes(req.method)) {
36
+ const originalResponse = res.end.bind(res);
37
+ const wrappedResponse = (...args) => {
38
+ if (res.statusCode >= 200 && res.statusCode < 300) {
39
+ const key = `${req.baseUrl}${req.route.path}`.replace(baseURL, req.method.toLowerCase());
40
+ const formater = formatterMap[key];
41
+ if (formater) {
42
+ const data = {
43
+ user: { ...pick(req.user, ["did", "role", "fullName"]) },
44
+ id: formater.id,
45
+ resource: formater.resource,
46
+ payload: formater.format({ req, res }),
47
+ ip: req.get("x-Real-ip", ""),
48
+ ua: req.get("user-agent", "")
49
+ };
50
+ writer(data);
51
+ }
52
+ }
53
+ return originalResponse(...args);
54
+ };
55
+ res.end = wrappedResponse;
56
+ }
57
+ next();
58
+ };
59
+ export {
60
+ DEFAULT_PAGE,
61
+ DEFAULT_PAGING_SIZE,
62
+ MAX_PAGING_SIZE,
63
+ createAuditMiddleware,
64
+ pagination,
65
+ validate
66
+ };
@@ -0,0 +1,29 @@
1
+ import BlockletNotification from "@blocklet/sdk/service/notification";
2
+ class Notification {
3
+ constructor({ logger, auth }) {
4
+ this.auth = auth;
5
+ this.logger = logger || { info: console.info, error: console.error };
6
+ }
7
+ async sendNotification({ to, title, message, actions, attachments = [], assetAddress }) {
8
+ try {
9
+ const payload = { title, body: message, actions: actions || [], attachments: [...attachments] };
10
+ if (typeof assetAddress !== "undefined") {
11
+ payload.attachments.push({
12
+ type: "asset",
13
+ data: {
14
+ chainHost: this.auth.chainHost,
15
+ did: assetAddress
16
+ }
17
+ });
18
+ }
19
+ await BlockletNotification.sendToUser(to, payload);
20
+ this.logger.info("text message was sent", { to, payload: JSON.stringify(payload, null, 2) });
21
+ } catch (error) {
22
+ this.logger.error("send text message failed", { error, to, message, actions, attachments });
23
+ throw error;
24
+ }
25
+ }
26
+ }
27
+ export {
28
+ Notification as default
29
+ };
@@ -0,0 +1,5 @@
1
+ const ADMIN_ROLES = ["owner", "admin"];
2
+ const canAccessAdmin = (role) => ADMIN_ROLES.includes(role);
3
+ export {
4
+ canAccessAdmin
5
+ };
package/es/util.js ADDED
@@ -0,0 +1,132 @@
1
+ import "moment-timezone";
2
+ import "moment/locale/zh-cn";
3
+ import moment from "moment";
4
+ import joinURL from "url-join";
5
+ import get from "lodash.get";
6
+ import { TIME_LOCALE, LAUNCH_STATUS } from "./constant";
7
+ const formatPeriod = (duration) => {
8
+ const value = Number(duration.slice(0, duration.length - 1));
9
+ const unit = duration[duration.length - 1];
10
+ return { value, unit };
11
+ };
12
+ const prettyDurationUnit = ({ value, unit }, locale) => {
13
+ if (!Object.keys(TIME_LOCALE).includes(locale)) {
14
+ locale = "en";
15
+ }
16
+ const localeKey = value > 1 ? `${unit}s` : unit;
17
+ return TIME_LOCALE[locale][localeKey.toLowerCase()];
18
+ };
19
+ const prettyDuration = (duration, locale) => {
20
+ if (!duration) {
21
+ return "";
22
+ }
23
+ const { value, unit } = duration;
24
+ return `${value}${prettyDurationUnit({ value, unit }, locale)}`;
25
+ };
26
+ const formatDatetime = (time, locale = "en-us", timezone = Intl.DateTimeFormat().resolvedOptions().timeZone) => {
27
+ if (!time) {
28
+ return "";
29
+ }
30
+ locale = locale || "en-us";
31
+ if (locale === "zh") {
32
+ locale = "zh-cn";
33
+ }
34
+ try {
35
+ return moment(time).locale(locale).tz(timezone).format("LLL zz");
36
+ } catch (error) {
37
+ console.error(`formate date time "${time}" error`, error);
38
+ return "";
39
+ }
40
+ };
41
+ const formatUtcDatetime = (time, locale = "en-us") => formatDatetime(time, locale, "UTC");
42
+ const sortArrayByDate = (array, asc = true, field = void 0) => (array || []).sort((x, y) => {
43
+ let v1 = x;
44
+ let v2 = y;
45
+ if (typeof field !== "undefined") {
46
+ v1 = v1[field];
47
+ v2 = v2[field];
48
+ }
49
+ if (v1 === v2) {
50
+ return 0;
51
+ }
52
+ const directionResult = asc ? 1 : -1;
53
+ if (moment(v1).diff(moment(v2)) > 0) {
54
+ return directionResult;
55
+ }
56
+ return -1 * directionResult;
57
+ });
58
+ const getSort = (sortby, sortdir) => {
59
+ if (sortby && sortby !== "undefined") {
60
+ return { [sortby]: sortdir };
61
+ }
62
+ return { createdAt: -1 };
63
+ };
64
+ const getExplorerUrl = ({ address, type = "txs", chainHost }) => {
65
+ return `https://explorer.abtnetwork.io/explorer/${type}/${address}?host=${chainHost}`;
66
+ };
67
+ const getBlockletDisplayName = (blocklet) => get(blocklet, "title") || get(blocklet, "name") || "";
68
+ const getBlockletAdminURL = (appURL) => joinURL(appURL, "/.well-known/service/admin/overview");
69
+ const isDateExpired = (expirationDate) => !!expirationDate && new Date(expirationDate).getTime() <= Date.now();
70
+ const getContinueLaunchURL = ({ baseURL, launch }) => {
71
+ const urlObject = new URL(baseURL);
72
+ if (launch.status < LAUNCH_STATUS.paid) {
73
+ urlObject.searchParams.set("sessionId", launch._id);
74
+ urlObject.searchParams.set("blocklet_meta_url", launch.blockletMetaUrl);
75
+ return urlObject.href;
76
+ }
77
+ if (launch.status >= LAUNCH_STATUS.paid && launch.status < LAUNCH_STATUS.installed) {
78
+ urlObject.pathname = joinURL(urlObject.pathname, `/launch/${launch.nftDid}`);
79
+ urlObject.searchParams.set("sessionId", launch._id);
80
+ urlObject.searchParams.set("blocklet_meta_url", launch.blockletMetaUrl);
81
+ urlObject.searchParams.set("launchType", launch.type);
82
+ if (launch.from) {
83
+ urlObject.searchParams.set("from", launch.from);
84
+ }
85
+ return urlObject.href;
86
+ }
87
+ return "";
88
+ };
89
+ const getBlockletMetaUrlFromQuery = (query) => {
90
+ const url = (query.get("blocklet_meta_url") || query.get("meta_url") || "").trim();
91
+ return decodeURIComponent(url);
92
+ };
93
+ const getRegistryUrlFromBlockletMetaUrl = (blockletMetaUrl) => {
94
+ try {
95
+ return blockletMetaUrl ? new URL(blockletMetaUrl).origin : "";
96
+ } catch (error) {
97
+ console.error("get registry url from blocklet meta url error:", error);
98
+ return "";
99
+ }
100
+ };
101
+ const formatRegistryLogoPath = (did, asset) => {
102
+ if (asset.startsWith("/assets")) {
103
+ return asset;
104
+ }
105
+ return `/assets/${did}/${asset}`;
106
+ };
107
+ const getBlockletLogoUrl = ({ did, baseUrl, logoPath }) => {
108
+ if (logoPath && logoPath.startsWith("http")) {
109
+ return logoPath;
110
+ }
111
+ if (baseUrl.startsWith("http") && logoPath) {
112
+ return joinURL(baseUrl, formatRegistryLogoPath(did, logoPath));
113
+ }
114
+ return "";
115
+ };
116
+ export {
117
+ formatDatetime,
118
+ formatPeriod,
119
+ formatUtcDatetime,
120
+ getBlockletAdminURL,
121
+ getBlockletDisplayName,
122
+ getBlockletLogoUrl,
123
+ getBlockletMetaUrlFromQuery,
124
+ getContinueLaunchURL,
125
+ getExplorerUrl,
126
+ getRegistryUrlFromBlockletMetaUrl,
127
+ getSort,
128
+ isDateExpired,
129
+ prettyDuration,
130
+ prettyDurationUnit,
131
+ sortArrayByDate
132
+ };
@@ -0,0 +1,10 @@
1
+ const create = (schema) => (data, locale) => {
2
+ const { error, value } = schema.validate(data, { errors: { language: locale || "en" } });
3
+ if (error) {
4
+ throw new Error(error.message);
5
+ }
6
+ return value;
7
+ };
8
+ export {
9
+ create
10
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/launcher-util",
3
- "version": "2.2.3",
3
+ "version": "2.2.5",
4
4
  "description": "Common constants",
5
5
  "keywords": [
6
6
  "constant"
@@ -11,6 +11,7 @@
11
11
  "main": "lib/util.js",
12
12
  "files": [
13
13
  "lib",
14
+ "es",
14
15
  "LICENSE",
15
16
  "package.json",
16
17
  "README.md"
@@ -50,5 +51,5 @@
50
51
  "vite": "^4.4.9",
51
52
  "vite-plugin-build": "^0.10.0"
52
53
  },
53
- "gitHead": "1acf4153e622cc49e553eff6366b3516c1aa6b7a"
54
+ "gitHead": "fab4f0dd5e6475b7e96104219aec4da5262df3ef"
54
55
  }