@ledgerhq/hw-app-str 6.24.1 → 6.25.1-alpha.3
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-es/Str.js.flow +316 -0
- package/lib-es/utils.js.flow +116 -0
- package/package.json +3 -3
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Ledger Node JS API
|
|
3
|
+
* (c) 2017-2018 Ledger
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
********************************************************************************/
|
|
17
|
+
//@flow
|
|
18
|
+
|
|
19
|
+
import type Transport from "@ledgerhq/hw-transport";
|
|
20
|
+
import {
|
|
21
|
+
splitPath,
|
|
22
|
+
foreach,
|
|
23
|
+
encodeEd25519PublicKey,
|
|
24
|
+
verifyEd25519Signature,
|
|
25
|
+
checkStellarBip32Path,
|
|
26
|
+
hash,
|
|
27
|
+
} from "./utils";
|
|
28
|
+
|
|
29
|
+
const CLA = 0xe0;
|
|
30
|
+
const INS_GET_PK = 0x02;
|
|
31
|
+
const INS_SIGN_TX = 0x04;
|
|
32
|
+
const INS_GET_CONF = 0x06;
|
|
33
|
+
const INS_SIGN_TX_HASH = 0x08;
|
|
34
|
+
const INS_KEEP_ALIVE = 0x10;
|
|
35
|
+
|
|
36
|
+
const APDU_MAX_SIZE = 150;
|
|
37
|
+
const P1_FIRST_APDU = 0x00;
|
|
38
|
+
const P1_MORE_APDU = 0x80;
|
|
39
|
+
const P2_LAST_APDU = 0x00;
|
|
40
|
+
const P2_MORE_APDU = 0x80;
|
|
41
|
+
|
|
42
|
+
const SW_OK = 0x9000;
|
|
43
|
+
const SW_CANCEL = 0x6985;
|
|
44
|
+
const SW_UNKNOWN_OP = 0x6c24;
|
|
45
|
+
const SW_MULTI_OP = 0x6c25;
|
|
46
|
+
const SW_NOT_ALLOWED = 0x6c66;
|
|
47
|
+
const SW_UNSUPPORTED = 0x6d00;
|
|
48
|
+
const SW_KEEP_ALIVE = 0x6e02;
|
|
49
|
+
|
|
50
|
+
const TX_MAX_SIZE = 1540;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Stellar API
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* import Str from "@ledgerhq/hw-app-str";
|
|
57
|
+
* const str = new Str(transport)
|
|
58
|
+
*/
|
|
59
|
+
export default class Str {
|
|
60
|
+
transport: Transport<*>;
|
|
61
|
+
|
|
62
|
+
constructor(transport: Transport<*>, scrambleKey: string = "l0v") {
|
|
63
|
+
this.transport = transport;
|
|
64
|
+
transport.decorateAppAPIMethods(
|
|
65
|
+
this,
|
|
66
|
+
["getAppConfiguration", "getPublicKey", "signTransaction", "signHash"],
|
|
67
|
+
scrambleKey
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getAppConfiguration(): Promise<{
|
|
72
|
+
version: string,
|
|
73
|
+
}> {
|
|
74
|
+
return this.transport
|
|
75
|
+
.send(CLA, INS_GET_CONF, 0x00, 0x00)
|
|
76
|
+
.then((response) => {
|
|
77
|
+
let multiOpsEnabled = response[0] === 0x01 || response[1] < 0x02;
|
|
78
|
+
let version = "" + response[1] + "." + response[2] + "." + response[3];
|
|
79
|
+
return {
|
|
80
|
+
version: version,
|
|
81
|
+
multiOpsEnabled: multiOpsEnabled,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* get Stellar public key for a given BIP 32 path.
|
|
88
|
+
* @param path a path in BIP 32 format
|
|
89
|
+
* @option boolValidate optionally enable key pair validation
|
|
90
|
+
* @option boolDisplay optionally enable or not the display
|
|
91
|
+
* @return an object with the publicKey (using XLM public key format) and
|
|
92
|
+
* the raw ed25519 public key.
|
|
93
|
+
* @example
|
|
94
|
+
* str.getPublicKey("44'/148'/0'").then(o => o.publicKey)
|
|
95
|
+
*/
|
|
96
|
+
getPublicKey(
|
|
97
|
+
path: string,
|
|
98
|
+
boolValidate?: boolean,
|
|
99
|
+
boolDisplay?: boolean
|
|
100
|
+
): Promise<{ publicKey: string, raw: Buffer }> {
|
|
101
|
+
checkStellarBip32Path(path);
|
|
102
|
+
|
|
103
|
+
let apdus = [];
|
|
104
|
+
let response;
|
|
105
|
+
|
|
106
|
+
let pathElts = splitPath(path);
|
|
107
|
+
let buffer = Buffer.alloc(1 + pathElts.length * 4);
|
|
108
|
+
buffer[0] = pathElts.length;
|
|
109
|
+
pathElts.forEach((element, index) => {
|
|
110
|
+
buffer.writeUInt32BE(element, 1 + 4 * index);
|
|
111
|
+
});
|
|
112
|
+
let verifyMsg = Buffer.from("via lumina", "ascii");
|
|
113
|
+
apdus.push(Buffer.concat([buffer, verifyMsg]));
|
|
114
|
+
let keepAlive = false;
|
|
115
|
+
return foreach(apdus, (data) =>
|
|
116
|
+
this.transport
|
|
117
|
+
.send(
|
|
118
|
+
CLA,
|
|
119
|
+
keepAlive ? INS_KEEP_ALIVE : INS_GET_PK,
|
|
120
|
+
boolValidate ? 0x01 : 0x00,
|
|
121
|
+
boolDisplay ? 0x01 : 0x00,
|
|
122
|
+
data,
|
|
123
|
+
[SW_OK, SW_KEEP_ALIVE]
|
|
124
|
+
)
|
|
125
|
+
.then((apduResponse) => {
|
|
126
|
+
let status = Buffer.from(
|
|
127
|
+
apduResponse.slice(apduResponse.length - 2)
|
|
128
|
+
).readUInt16BE(0);
|
|
129
|
+
if (status === SW_KEEP_ALIVE) {
|
|
130
|
+
keepAlive = true;
|
|
131
|
+
apdus.push(Buffer.alloc(0));
|
|
132
|
+
}
|
|
133
|
+
response = apduResponse;
|
|
134
|
+
})
|
|
135
|
+
).then(() => {
|
|
136
|
+
// response = Buffer.from(response, 'hex');
|
|
137
|
+
let offset = 0;
|
|
138
|
+
let rawPublicKey = response.slice(offset, offset + 32);
|
|
139
|
+
offset += 32;
|
|
140
|
+
let publicKey = encodeEd25519PublicKey(rawPublicKey);
|
|
141
|
+
if (boolValidate) {
|
|
142
|
+
let signature = response.slice(offset, offset + 64);
|
|
143
|
+
if (!verifyEd25519Signature(verifyMsg, signature, rawPublicKey)) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
"Bad signature. Keypair is invalid. Please report this."
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
publicKey: publicKey,
|
|
151
|
+
raw: rawPublicKey,
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* sign a Stellar transaction.
|
|
158
|
+
* @param path a path in BIP 32 format
|
|
159
|
+
* @param transaction signature base of the transaction to sign
|
|
160
|
+
* @return an object with the signature and the status
|
|
161
|
+
* @example
|
|
162
|
+
* str.signTransaction("44'/148'/0'", signatureBase).then(o => o.signature)
|
|
163
|
+
*/
|
|
164
|
+
signTransaction(
|
|
165
|
+
path: string,
|
|
166
|
+
transaction: Buffer
|
|
167
|
+
): Promise<{ signature: Buffer }> {
|
|
168
|
+
checkStellarBip32Path(path);
|
|
169
|
+
|
|
170
|
+
if (transaction.length > TX_MAX_SIZE) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
"Transaction too large: max = " +
|
|
173
|
+
TX_MAX_SIZE +
|
|
174
|
+
"; actual = " +
|
|
175
|
+
transaction.length
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let apdus = [];
|
|
180
|
+
let response;
|
|
181
|
+
|
|
182
|
+
let pathElts = splitPath(path);
|
|
183
|
+
let bufferSize = 1 + pathElts.length * 4;
|
|
184
|
+
let buffer = Buffer.alloc(bufferSize);
|
|
185
|
+
buffer[0] = pathElts.length;
|
|
186
|
+
pathElts.forEach(function (element, index) {
|
|
187
|
+
buffer.writeUInt32BE(element, 1 + 4 * index);
|
|
188
|
+
});
|
|
189
|
+
let chunkSize = APDU_MAX_SIZE - bufferSize;
|
|
190
|
+
if (transaction.length <= chunkSize) {
|
|
191
|
+
// it fits in a single apdu
|
|
192
|
+
apdus.push(Buffer.concat([buffer, transaction]));
|
|
193
|
+
} else {
|
|
194
|
+
// we need to send multiple apdus to transmit the entire transaction
|
|
195
|
+
let chunk = Buffer.alloc(chunkSize);
|
|
196
|
+
let offset = 0;
|
|
197
|
+
transaction.copy(chunk, 0, offset, chunkSize);
|
|
198
|
+
apdus.push(Buffer.concat([buffer, chunk]));
|
|
199
|
+
offset += chunkSize;
|
|
200
|
+
while (offset < transaction.length) {
|
|
201
|
+
let remaining = transaction.length - offset;
|
|
202
|
+
chunkSize = remaining < APDU_MAX_SIZE ? remaining : APDU_MAX_SIZE;
|
|
203
|
+
chunk = Buffer.alloc(chunkSize);
|
|
204
|
+
transaction.copy(chunk, 0, offset, offset + chunkSize);
|
|
205
|
+
offset += chunkSize;
|
|
206
|
+
apdus.push(chunk);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
let keepAlive = false;
|
|
210
|
+
return foreach(apdus, (data, i) =>
|
|
211
|
+
this.transport
|
|
212
|
+
.send(
|
|
213
|
+
CLA,
|
|
214
|
+
keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX,
|
|
215
|
+
i === 0 ? P1_FIRST_APDU : P1_MORE_APDU,
|
|
216
|
+
i === apdus.length - 1 ? P2_LAST_APDU : P2_MORE_APDU,
|
|
217
|
+
data,
|
|
218
|
+
[SW_OK, SW_CANCEL, SW_UNKNOWN_OP, SW_MULTI_OP, SW_KEEP_ALIVE]
|
|
219
|
+
)
|
|
220
|
+
.then((apduResponse) => {
|
|
221
|
+
let status = Buffer.from(
|
|
222
|
+
apduResponse.slice(apduResponse.length - 2)
|
|
223
|
+
).readUInt16BE(0);
|
|
224
|
+
if (status === SW_KEEP_ALIVE) {
|
|
225
|
+
keepAlive = true;
|
|
226
|
+
apdus.push(Buffer.alloc(0));
|
|
227
|
+
}
|
|
228
|
+
response = apduResponse;
|
|
229
|
+
})
|
|
230
|
+
).then(() => {
|
|
231
|
+
let status = Buffer.from(
|
|
232
|
+
response.slice(response.length - 2)
|
|
233
|
+
).readUInt16BE(0);
|
|
234
|
+
if (status === SW_OK) {
|
|
235
|
+
let signature = Buffer.from(response.slice(0, response.length - 2));
|
|
236
|
+
return {
|
|
237
|
+
signature: signature,
|
|
238
|
+
};
|
|
239
|
+
} else if (status === SW_UNKNOWN_OP) {
|
|
240
|
+
// pre-v2 app version: fall back on hash signing
|
|
241
|
+
return this.signHash_private(path, hash(transaction));
|
|
242
|
+
} else if (status === SW_MULTI_OP) {
|
|
243
|
+
// multi-operation transaction: attempt hash signing
|
|
244
|
+
return this.signHash_private(path, hash(transaction));
|
|
245
|
+
} else {
|
|
246
|
+
throw new Error("Transaction approval request was rejected");
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* sign a Stellar transaction hash.
|
|
253
|
+
* @param path a path in BIP 32 format
|
|
254
|
+
* @param hash hash of the transaction to sign
|
|
255
|
+
* @return an object with the signature
|
|
256
|
+
* @example
|
|
257
|
+
* str.signHash("44'/148'/0'", hash).then(o => o.signature)
|
|
258
|
+
*/
|
|
259
|
+
signHash(path: string, hash: Buffer): Promise<{ signature: Buffer }> {
|
|
260
|
+
checkStellarBip32Path(path);
|
|
261
|
+
return this.signHash_private(path, hash);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
signHash_private(path: string, hash: Buffer): Promise<{ signature: Buffer }> {
|
|
265
|
+
let apdus = [];
|
|
266
|
+
let response;
|
|
267
|
+
|
|
268
|
+
let pathElts = splitPath(path);
|
|
269
|
+
let buffer = Buffer.alloc(1 + pathElts.length * 4);
|
|
270
|
+
buffer[0] = pathElts.length;
|
|
271
|
+
pathElts.forEach(function (element, index) {
|
|
272
|
+
buffer.writeUInt32BE(element, 1 + 4 * index);
|
|
273
|
+
});
|
|
274
|
+
apdus.push(Buffer.concat([buffer, hash]));
|
|
275
|
+
let keepAlive = false;
|
|
276
|
+
return foreach(apdus, (data) =>
|
|
277
|
+
this.transport
|
|
278
|
+
.send(
|
|
279
|
+
CLA,
|
|
280
|
+
keepAlive ? INS_KEEP_ALIVE : INS_SIGN_TX_HASH,
|
|
281
|
+
0x00,
|
|
282
|
+
0x00,
|
|
283
|
+
data,
|
|
284
|
+
[SW_OK, SW_CANCEL, SW_NOT_ALLOWED, SW_UNSUPPORTED, SW_KEEP_ALIVE]
|
|
285
|
+
)
|
|
286
|
+
.then((apduResponse) => {
|
|
287
|
+
let status = Buffer.from(
|
|
288
|
+
apduResponse.slice(apduResponse.length - 2)
|
|
289
|
+
).readUInt16BE(0);
|
|
290
|
+
if (status === SW_KEEP_ALIVE) {
|
|
291
|
+
keepAlive = true;
|
|
292
|
+
apdus.push(Buffer.alloc(0));
|
|
293
|
+
}
|
|
294
|
+
response = apduResponse;
|
|
295
|
+
})
|
|
296
|
+
).then(() => {
|
|
297
|
+
let status = Buffer.from(
|
|
298
|
+
response.slice(response.length - 2)
|
|
299
|
+
).readUInt16BE(0);
|
|
300
|
+
if (status === SW_OK) {
|
|
301
|
+
let signature = Buffer.from(response.slice(0, response.length - 2));
|
|
302
|
+
return {
|
|
303
|
+
signature: signature,
|
|
304
|
+
};
|
|
305
|
+
} else if (status === SW_CANCEL) {
|
|
306
|
+
throw new Error("Transaction approval request was rejected");
|
|
307
|
+
} else if (status === SW_UNSUPPORTED) {
|
|
308
|
+
throw new Error("Hash signing is not supported");
|
|
309
|
+
} else {
|
|
310
|
+
throw new Error(
|
|
311
|
+
"Hash signing not allowed. Have you enabled it in the app settings?"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Ledger Node JS API
|
|
3
|
+
* (c) 2017-2018 Ledger
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
********************************************************************************/
|
|
17
|
+
//@flow
|
|
18
|
+
|
|
19
|
+
import base32 from "base32.js";
|
|
20
|
+
import nacl from "tweetnacl";
|
|
21
|
+
import { sha256 } from "sha.js";
|
|
22
|
+
|
|
23
|
+
// TODO use bip32-path library
|
|
24
|
+
export function splitPath(path: string): number[] {
|
|
25
|
+
let result = [];
|
|
26
|
+
let components = path.split("/");
|
|
27
|
+
components.forEach((element) => {
|
|
28
|
+
let number = parseInt(element, 10);
|
|
29
|
+
if (isNaN(number)) {
|
|
30
|
+
return; // FIXME shouldn't it throws instead?
|
|
31
|
+
}
|
|
32
|
+
if (element.length > 1 && element[element.length - 1] === "'") {
|
|
33
|
+
number += 0x80000000;
|
|
34
|
+
}
|
|
35
|
+
result.push(number);
|
|
36
|
+
});
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function foreach<T, A>(
|
|
41
|
+
arr: T[],
|
|
42
|
+
callback: (T, number) => Promise<A>
|
|
43
|
+
): Promise<A[]> {
|
|
44
|
+
function iterate(index, array, result) {
|
|
45
|
+
if (index >= array.length) {
|
|
46
|
+
return result;
|
|
47
|
+
} else {
|
|
48
|
+
return callback(array[index], index).then(function (res) {
|
|
49
|
+
result.push(res);
|
|
50
|
+
return iterate(index + 1, array, result);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return Promise.resolve().then(() => iterate(0, arr, []));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function crc16xmodem(buf: Buffer, previous?: number): number {
|
|
58
|
+
let crc = typeof previous !== "undefined" ? ~~previous : 0x0;
|
|
59
|
+
|
|
60
|
+
for (var index = 0; index < buf.length; index++) {
|
|
61
|
+
const byte = buf[index];
|
|
62
|
+
let code = (crc >>> 8) & 0xff;
|
|
63
|
+
|
|
64
|
+
code ^= byte & 0xff;
|
|
65
|
+
code ^= code >>> 4;
|
|
66
|
+
crc = (crc << 8) & 0xffff;
|
|
67
|
+
crc ^= code;
|
|
68
|
+
code = (code << 5) & 0xffff;
|
|
69
|
+
crc ^= code;
|
|
70
|
+
code = (code << 7) & 0xffff;
|
|
71
|
+
crc ^= code;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return crc;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function encodeEd25519PublicKey(rawPublicKey: Buffer): string {
|
|
78
|
+
let versionByte = 6 << 3; // 'G'
|
|
79
|
+
let data = Buffer.from(rawPublicKey);
|
|
80
|
+
let versionBuffer = Buffer.from([versionByte]);
|
|
81
|
+
let payload = Buffer.concat([versionBuffer, data]);
|
|
82
|
+
let checksum = Buffer.alloc(2);
|
|
83
|
+
checksum.writeUInt16LE(crc16xmodem(payload), 0);
|
|
84
|
+
let unencoded = Buffer.concat([payload, checksum]);
|
|
85
|
+
return base32.encode(unencoded);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function verifyEd25519Signature(
|
|
89
|
+
data: Buffer,
|
|
90
|
+
signature: Buffer,
|
|
91
|
+
publicKey: Buffer
|
|
92
|
+
): boolean {
|
|
93
|
+
return nacl.sign.detached.verify(
|
|
94
|
+
new Uint8Array(data.toJSON().data),
|
|
95
|
+
new Uint8Array(signature.toJSON().data),
|
|
96
|
+
new Uint8Array(publicKey.toJSON().data)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function hash(data: Buffer) {
|
|
101
|
+
let hasher = new sha256();
|
|
102
|
+
hasher.update(data, "utf8");
|
|
103
|
+
return hasher.digest();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function checkStellarBip32Path(path: string): void {
|
|
107
|
+
path.split("/").forEach(function (element) {
|
|
108
|
+
if (!element.toString().endsWith("'")) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
"Detected a non-hardened path element in requested BIP32 path." +
|
|
111
|
+
" Non-hardended paths are not supported at this time. Please use an all-hardened path." +
|
|
112
|
+
" Example: 44'/148'/0'"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ledgerhq/hw-app-str",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.25.1-alpha.3+eb669e17",
|
|
4
4
|
"description": "Ledger Hardware Wallet Stellar Application API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Ledger",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"types": "lib/Str.d.ts",
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@ledgerhq/hw-transport": "^6.
|
|
30
|
+
"@ledgerhq/hw-transport": "^6.25.1-alpha.3+eb669e17",
|
|
31
31
|
"base32.js": "^0.1.0",
|
|
32
32
|
"sha.js": "^2.3.6",
|
|
33
33
|
"tweetnacl": "^1.0.3"
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"build": "bash ../../script/build.sh",
|
|
38
38
|
"watch": "bash ../../script/watch.sh"
|
|
39
39
|
},
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "eb669e17dd87d3ab568beab1f9a5ddb1a2536e83"
|
|
41
41
|
}
|