core-3nweb-client-lib 0.45.1 → 0.45.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/build/core/asmail/index.js +4 -4
- package/build/core/index.js +7 -7
- package/build/lib-client/doh.js +6 -6
- package/build/lib-client/logging/log-to-file.js +1 -0
- package/build/lib-client/service-checks.js +2 -1
- package/build/lib-client/service-locator.d.ts +2 -8
- package/build/lib-client/service-locator.js +10 -17
- package/build/lib-client/xsp-fs/folder-node.d.ts +1 -0
- package/build/lib-client/xsp-fs/folder-node.js +10 -0
- package/build/lib-client/xsp-fs/fs.js +43 -13
- package/package.json +3 -3
|
@@ -69,8 +69,8 @@ class ASMail {
|
|
|
69
69
|
address: this.address,
|
|
70
70
|
cryptor: this.cryptor,
|
|
71
71
|
getSigner,
|
|
72
|
-
asmailResolver: makeResolver('asmail'),
|
|
73
|
-
midResolver: makeResolver('mailerid'),
|
|
72
|
+
asmailResolver: makeResolver('asmail', this.logger.logError),
|
|
73
|
+
midResolver: makeResolver('mailerid', this.logger.logError),
|
|
74
74
|
correspondents: {
|
|
75
75
|
needIntroKeyFor: this.keyring.needIntroKeyFor,
|
|
76
76
|
generateKeysToSend: this.keyring.generateKeysToSend,
|
|
@@ -92,12 +92,12 @@ class ASMail {
|
|
|
92
92
|
cryptor: this.cryptor,
|
|
93
93
|
getSigner,
|
|
94
94
|
getStorages,
|
|
95
|
-
asmailResolver: makeResolver('asmail'),
|
|
95
|
+
asmailResolver: makeResolver('asmail', this.logger.logError),
|
|
96
96
|
correspondents: {
|
|
97
97
|
msgDecryptor: this.keyring.decrypt,
|
|
98
98
|
markOwnSendingParamsAsUsed: this.sendingParams.thisSide.setAsUsed,
|
|
99
99
|
saveParamsForSendingTo: this.sendingParams.otherSides.set,
|
|
100
|
-
midResolver: makeResolver('mailerid')
|
|
100
|
+
midResolver: makeResolver('mailerid', this.logger.logError)
|
|
101
101
|
},
|
|
102
102
|
makeNet: this.makeNet,
|
|
103
103
|
logError: this.logger.logError
|
package/build/core/index.js
CHANGED
|
@@ -93,7 +93,7 @@ class Core {
|
|
|
93
93
|
});
|
|
94
94
|
try {
|
|
95
95
|
// 1) init of id manager without setting fs
|
|
96
|
-
const stepTwo = await id_manager_1.IdManager.initWithoutStore(u.address, this.makeResolver('mailerid'), this.makeNet, this.logger.logError, this.logger.logWarning);
|
|
96
|
+
const stepTwo = await id_manager_1.IdManager.initWithoutStore(u.address, this.makeResolver('mailerid', this.logger.logError), this.makeNet, this.logger.logError, this.logger.logWarning);
|
|
97
97
|
if (!stepTwo) {
|
|
98
98
|
const message = `MailerId server doesn't recognize identity ${u.address}`;
|
|
99
99
|
emitBootEvent({ message, isError: true });
|
|
@@ -111,7 +111,7 @@ class Core {
|
|
|
111
111
|
const { idManager, setupManagerStorage } = idManagerInit;
|
|
112
112
|
// 3) initialize all storages
|
|
113
113
|
emitBootEvent({ message: `Setting up main storage for new user` });
|
|
114
|
-
const storesUp = await this.storages.initFreshForNewUser(u.address, idManager.getSigner, u.storeParams, u.storeSKey, this.makeNet, this.makeResolver('3nstorage'), this.logger.logError);
|
|
114
|
+
const storesUp = await this.storages.initFreshForNewUser(u.address, idManager.getSigner, u.storeParams, u.storeSKey, this.makeNet, this.makeResolver('3nstorage', this.logger.logError), this.logger.logError);
|
|
115
115
|
if (!storesUp) {
|
|
116
116
|
const message = `Main store failed to initialize for new user`;
|
|
117
117
|
emitBootEvent({ message, isError: true });
|
|
@@ -141,7 +141,7 @@ class Core {
|
|
|
141
141
|
});
|
|
142
142
|
try {
|
|
143
143
|
// 1) init of id manager without setting fs
|
|
144
|
-
const stepTwo = await id_manager_1.IdManager.initWithoutStore(address, this.makeResolver('mailerid'), this.makeNet, this.logger.logError, this.logger.logWarning);
|
|
144
|
+
const stepTwo = await id_manager_1.IdManager.initWithoutStore(address, this.makeResolver('mailerid', this.logger.logError), this.makeNet, this.logger.logError, this.logger.logWarning);
|
|
145
145
|
if (!stepTwo) {
|
|
146
146
|
emitBootEvent({
|
|
147
147
|
isError: true, message: `MailerId server doesn't recognize identity ${address}`
|
|
@@ -163,7 +163,7 @@ class Core {
|
|
|
163
163
|
const { idManager, setupManagerStorage } = idManagerInit;
|
|
164
164
|
// 3) initialize all storages
|
|
165
165
|
emitBootEvent({ message: `Setting up main storage without local cache` });
|
|
166
|
-
const storeDone = await this.storages.initFromRemote(address, idManager.getSigner, storageKey, this.makeNet, this.makeResolver('3nstorage'), this.logger.logError);
|
|
166
|
+
const storeDone = await this.storages.initFromRemote(address, idManager.getSigner, storageKey, this.makeNet, this.makeResolver('3nstorage', this.logger.logError), this.logger.logError);
|
|
167
167
|
if (!storeDone) {
|
|
168
168
|
emitBootEvent({ message: `Main store failed to initialize`, isError: true });
|
|
169
169
|
return false;
|
|
@@ -204,7 +204,7 @@ class Core {
|
|
|
204
204
|
});
|
|
205
205
|
try {
|
|
206
206
|
emitBootEvent({ message: `Unlocking data from local cache with provided password/key` });
|
|
207
|
-
const completeStorageInit = await this.storages.startInitFromCache(address, storageKey, this.makeNet, this.makeResolver('3nstorage'), this.logger.logError);
|
|
207
|
+
const completeStorageInit = await this.storages.startInitFromCache(address, storageKey, this.makeNet, this.makeResolver('3nstorage', this.logger.logError), this.logger.logError);
|
|
208
208
|
if (!completeStorageInit) {
|
|
209
209
|
emitBootEvent({
|
|
210
210
|
isError: true, message: `password/key is incorrect to decrypt local caches, or caches are damaged`
|
|
@@ -212,7 +212,7 @@ class Core {
|
|
|
212
212
|
return false;
|
|
213
213
|
}
|
|
214
214
|
emitBootEvent({ message: `✔️ main storage is opened` });
|
|
215
|
-
const idManager = await id_manager_1.IdManager.initFromCachedStore(address, await this.storages.makeSyncedFSForApp(constants_1.MAILERID_APP_NAME), this.makeResolver('mailerid'), this.makeNet, this.logger.logError, this.logger.logWarning);
|
|
215
|
+
const idManager = await id_manager_1.IdManager.initFromCachedStore(address, await this.storages.makeSyncedFSForApp(constants_1.MAILERID_APP_NAME), this.makeResolver('mailerid', this.logger.logError), this.makeNet, this.logger.logError, this.logger.logWarning);
|
|
216
216
|
if (!idManager) {
|
|
217
217
|
return false;
|
|
218
218
|
}
|
|
@@ -319,7 +319,7 @@ class Core {
|
|
|
319
319
|
try {
|
|
320
320
|
const address = this.idManager.getId();
|
|
321
321
|
const getSigner = this.idManager.getSigner;
|
|
322
|
-
const asmailServerConfig = new config_1.ConfigOfASMailServer(address, getSigner, this.makeResolver('asmail'), this.makeNet());
|
|
322
|
+
const asmailServerConfig = new config_1.ConfigOfASMailServer(address, getSigner, this.makeResolver('asmail', this.logger.logError), this.makeNet());
|
|
323
323
|
emitBootEvent({ coreApp: constants_1.KEYRINGS_APP_NAME, message: `starting initialization` });
|
|
324
324
|
const keyringsSyncedFS = await this.storages.makeSyncedFSForApp(constants_1.KEYRINGS_APP_NAME);
|
|
325
325
|
await this.keyrings.init(keyringsSyncedFS, this.idManager.getSigner, asmailServerConfig.makeParamSetterAndGetter('init-pub-key'));
|
package/build/lib-client/doh.js
CHANGED
|
@@ -19,7 +19,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
19
19
|
exports.dohAt = dohAt;
|
|
20
20
|
const https = require("https");
|
|
21
21
|
const request_utils_1 = require("./request-utils");
|
|
22
|
-
const
|
|
22
|
+
const dns_1 = require("dns");
|
|
23
23
|
function dohAt(dohServerUrl) {
|
|
24
24
|
async function resolveTxt(domain) {
|
|
25
25
|
const answer = await sendDohQuestion(dohServerUrl, domain, 'TXT');
|
|
@@ -42,27 +42,27 @@ async function sendDohQuestion(serverUrl, domain, type) {
|
|
|
42
42
|
const reply = await (0, request_utils_1.processRequest)(opts => https.request(opts), httpsOpts, opts, undefined)
|
|
43
43
|
.catch((exc) => {
|
|
44
44
|
if (exc.type === 'connect') {
|
|
45
|
-
throw { code:
|
|
45
|
+
throw { code: dns_1.CONNREFUSED, hostname: domain, cause: exc };
|
|
46
46
|
}
|
|
47
47
|
else {
|
|
48
|
-
throw { code:
|
|
48
|
+
throw { code: dns_1.SERVFAIL, hostname: domain, cause: exc };
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
const { status, data } = reply;
|
|
52
52
|
if (status !== 200) {
|
|
53
53
|
throw {
|
|
54
|
-
code:
|
|
54
|
+
code: dns_1.SERVFAIL, hostname: domain,
|
|
55
55
|
message: `status ${reply.status} from DoH server`
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
if (data.Status !== 0) {
|
|
59
|
-
throw { code:
|
|
59
|
+
throw { code: dns_1.NOTFOUND, hostname: domain };
|
|
60
60
|
}
|
|
61
61
|
if (data.Answer) {
|
|
62
62
|
return data.Answer;
|
|
63
63
|
}
|
|
64
64
|
else {
|
|
65
|
-
throw { code:
|
|
65
|
+
throw { code: dns_1.NODATA, hostname: domain };
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
Object.freeze(exports);
|
|
@@ -130,7 +130,7 @@ async function checkSignup(client, signupURL, signupToken) {
|
|
|
130
130
|
const srvLocator = (0, service_locator_1.makeServiceLocator)({ resolveTxt: dns_1.promises.resolveTxt }, (0, doh_1.dohAt)(`https://cloudflare-dns.com/dns-query`), (0, doh_1.dohAt)(`https://dns.google/resolve`));
|
|
131
131
|
async function checkUserDomainDNS(service, domain) {
|
|
132
132
|
try {
|
|
133
|
-
const serviceUrl = await (srvLocator(service))(`u@${domain}`);
|
|
133
|
+
const serviceUrl = await (srvLocator(service, noop))(`u@${domain}`);
|
|
134
134
|
return {
|
|
135
135
|
service,
|
|
136
136
|
isOk: true,
|
|
@@ -166,4 +166,5 @@ async function checkUserDomainDNS(service, domain) {
|
|
|
166
166
|
};
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
|
+
async function noop() { }
|
|
169
170
|
Object.freeze(exports);
|
|
@@ -2,6 +2,7 @@ import { NetClient } from './request-utils';
|
|
|
2
2
|
import { promises as dnsPromises } from 'dns';
|
|
3
3
|
import { StorageRootRoute } from '../lib-common/service-api/3nstorage/root-route';
|
|
4
4
|
import { ASMailRootRoute } from '../lib-common/service-api/asmail/root-route';
|
|
5
|
+
import { LogError } from './logging/log-to-file';
|
|
5
6
|
type SignedLoad = web3n.keys.SignedLoad;
|
|
6
7
|
/**
|
|
7
8
|
* This returns a promise, resolvable to ASMailRootRoute object.
|
|
@@ -26,15 +27,8 @@ export declare function mailerIdInfoAt(client: NetClient, url: string): Promise<
|
|
|
26
27
|
* @param url
|
|
27
28
|
*/
|
|
28
29
|
export declare function storageInfoAt(client: NetClient, url: string): Promise<StorageRootRoute>;
|
|
29
|
-
export declare const DNS_ERR_CODE: {
|
|
30
|
-
NODATA: string;
|
|
31
|
-
NOTFOUND: string;
|
|
32
|
-
ESERVFAIL: string;
|
|
33
|
-
ECONNREFUSED: string;
|
|
34
|
-
ETIMEOUT: string;
|
|
35
|
-
};
|
|
36
30
|
export type ServiceTypeDNSLabel = 'mailerid' | 'asmail' | '3nstorage';
|
|
37
|
-
export type ServiceLocatorMaker = (serviceLabel: ServiceTypeDNSLabel) => ServiceLocator;
|
|
31
|
+
export type ServiceLocatorMaker = (serviceLabel: ServiceTypeDNSLabel, logError: LogError) => ServiceLocator;
|
|
38
32
|
export type ServiceLocator = (address: string) => Promise<string>;
|
|
39
33
|
export interface DnsResolver {
|
|
40
34
|
resolveTxt: (typeof dnsPromises)['resolveTxt'];
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.DNS_ERR_CODE = void 0;
|
|
20
19
|
exports.asmailInfoAt = asmailInfoAt;
|
|
21
20
|
exports.mailerIdInfoAt = mailerIdInfoAt;
|
|
22
21
|
exports.storageInfoAt = storageInfoAt;
|
|
23
22
|
exports.makeServiceLocator = makeServiceLocator;
|
|
24
23
|
exports.getMailerIdInfoFor = getMailerIdInfoFor;
|
|
25
24
|
const jwkeys_1 = require("../lib-common/jwkeys");
|
|
25
|
+
const dns_1 = require("dns");
|
|
26
26
|
const runtime_1 = require("../lib-common/exceptions/runtime");
|
|
27
27
|
const http_1 = require("../lib-common/exceptions/http");
|
|
28
28
|
async function readJSONLocatedAt(client, url) {
|
|
@@ -216,22 +216,15 @@ function getRecordAtStartOf(txt) {
|
|
|
216
216
|
value: txt
|
|
217
217
|
};
|
|
218
218
|
}
|
|
219
|
-
exports.DNS_ERR_CODE = {
|
|
220
|
-
NODATA: 'ENODATA',
|
|
221
|
-
NOTFOUND: 'ENOTFOUND',
|
|
222
|
-
ESERVFAIL: 'ESERVFAIL',
|
|
223
|
-
ECONNREFUSED: 'ECONNREFUSED',
|
|
224
|
-
ETIMEOUT: 'ETIMEOUT'
|
|
225
|
-
};
|
|
226
|
-
Object.freeze(exports.DNS_ERR_CODE);
|
|
227
219
|
function makeServiceLocator(...resolvers) {
|
|
228
220
|
if (resolvers.length === 0) {
|
|
229
221
|
throw Error(`no DNS resolvers given`);
|
|
230
222
|
}
|
|
231
|
-
return serviceLabel => async (address) => {
|
|
223
|
+
return (serviceLabel, logError) => async (address) => {
|
|
232
224
|
const domain = domainOfAddress(address);
|
|
233
225
|
let prevConnectionExc = undefined;
|
|
234
|
-
for (
|
|
226
|
+
for (let i = 0; i < resolvers.length; i += 1) {
|
|
227
|
+
const resolver = resolvers[i];
|
|
235
228
|
try {
|
|
236
229
|
const txtRecords = await resolver.resolveTxt(domain);
|
|
237
230
|
const recValue = extractPair(txtRecords, serviceLabel);
|
|
@@ -242,25 +235,25 @@ function makeServiceLocator(...resolvers) {
|
|
|
242
235
|
return url;
|
|
243
236
|
}
|
|
244
237
|
catch (err) {
|
|
238
|
+
await logError(err, `Resolver ${i + 1} fails to get TXT records of ${domain}`);
|
|
245
239
|
const { code, hostname, message } = err;
|
|
246
|
-
if (code ===
|
|
240
|
+
if (code === dns_1.NODATA) {
|
|
247
241
|
throw noServiceRecordExc(address);
|
|
248
242
|
}
|
|
249
|
-
else if ((code ===
|
|
250
|
-
|| (code ===
|
|
251
|
-
|| (code ===
|
|
243
|
+
else if ((code === dns_1.SERVFAIL)
|
|
244
|
+
|| (code === dns_1.CONNREFUSED)
|
|
245
|
+
|| (code === dns_1.TIMEOUT)) {
|
|
252
246
|
if (!prevConnectionExc) {
|
|
253
247
|
prevConnectionExc = noConnectionExc({ code, hostname, message });
|
|
254
248
|
}
|
|
255
249
|
}
|
|
256
|
-
else if ((code ===
|
|
250
|
+
else if ((code === dns_1.NOTFOUND) || hostname) {
|
|
257
251
|
throw domainNotFoundExc(address, { code, hostname, message });
|
|
258
252
|
}
|
|
259
253
|
else {
|
|
260
254
|
if (!prevConnectionExc) {
|
|
261
255
|
prevConnectionExc = err;
|
|
262
256
|
}
|
|
263
|
-
throw err;
|
|
264
257
|
}
|
|
265
258
|
}
|
|
266
259
|
}
|
|
@@ -143,6 +143,7 @@ export declare class FolderNode extends NodeInFS<FolderPersistance> {
|
|
|
143
143
|
createFile(name: string, exclusive: boolean): Promise<FileNode>;
|
|
144
144
|
createLink(name: string, params: LinkParameters<any>): Promise<void>;
|
|
145
145
|
removeChild(f: NodeInFS<any>): Promise<void>;
|
|
146
|
+
removeChildEntryUnconditionally(name: string): Promise<void>;
|
|
146
147
|
private changeChildName;
|
|
147
148
|
moveChildTo(childName: string, dst: FolderNode, nameInDst: string): Promise<void>;
|
|
148
149
|
private moveChildOut;
|
|
@@ -612,6 +612,16 @@ class FolderNode extends node_in_fs_1.NodeInFS {
|
|
|
612
612
|
// stays consistent
|
|
613
613
|
this.callRemoveObjOn('local', f);
|
|
614
614
|
}
|
|
615
|
+
async removeChildEntryUnconditionally(name) {
|
|
616
|
+
await this.doTransition(async (state, version) => {
|
|
617
|
+
const child = state.nodes[name];
|
|
618
|
+
if (!child) {
|
|
619
|
+
return [];
|
|
620
|
+
}
|
|
621
|
+
delete state.nodes[name];
|
|
622
|
+
return this.makeEntryRemovalEvent(version, 'local', child);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
615
625
|
changeChildName(oldName, newName) {
|
|
616
626
|
return this.doTransition(async (state, version) => {
|
|
617
627
|
const child = state.nodes[oldName];
|
|
@@ -146,32 +146,62 @@ class XspFS {
|
|
|
146
146
|
if (typeof folderName !== 'string') {
|
|
147
147
|
throw new Error('Cannot remove root folder');
|
|
148
148
|
}
|
|
149
|
-
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
try {
|
|
150
|
+
const folder = (await parentFolder.getFolder(folderName)
|
|
151
|
+
.catch(setExcPath(path)));
|
|
152
|
+
if (!removeContent && !folder.isEmpty()) {
|
|
153
|
+
throw (0, file_1.makeFileException)('notEmpty', path);
|
|
154
|
+
}
|
|
155
|
+
// note that internal folder.delete() removes all children as a matter
|
|
156
|
+
// of not leaving inaccessible nodes, i.e. content is removed implicitly
|
|
157
|
+
await parentFolder.removeChild(folder);
|
|
158
|
+
}
|
|
159
|
+
catch (exc) {
|
|
160
|
+
if (exc.type === 'secondary') {
|
|
161
|
+
await parentFolder.removeChildEntryUnconditionally(folderName);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
throw exc;
|
|
165
|
+
}
|
|
153
166
|
}
|
|
154
|
-
// note that internal folder.delete() removes all children as a matter
|
|
155
|
-
// of not leaving inaccessible nodes, i.e. content is removed implicitly
|
|
156
|
-
await parentFolder.removeChild(folder);
|
|
157
167
|
}
|
|
158
168
|
async deleteFile(path) {
|
|
159
169
|
const { fileName, folderPath } = split(path);
|
|
160
170
|
const root = this.v.getRootIfNotClosed(path);
|
|
161
171
|
const parentFolder = await root.getFolderInThisSubTree(folderPath)
|
|
162
172
|
.catch(setExcPath(path));
|
|
163
|
-
|
|
164
|
-
.
|
|
165
|
-
|
|
173
|
+
try {
|
|
174
|
+
const file = await parentFolder.getFile(fileName)
|
|
175
|
+
.catch(setExcPath(path));
|
|
176
|
+
await parentFolder.removeChild(file);
|
|
177
|
+
}
|
|
178
|
+
catch (exc) {
|
|
179
|
+
if (exc.type === 'secondary') {
|
|
180
|
+
await parentFolder.removeChildEntryUnconditionally(fileName);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
throw exc;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
166
186
|
}
|
|
167
187
|
async deleteLink(path) {
|
|
168
188
|
const { fileName, folderPath } = split(path);
|
|
169
189
|
const root = this.v.getRootIfNotClosed(path);
|
|
170
190
|
const parentFolder = await root.getFolderInThisSubTree(folderPath)
|
|
171
191
|
.catch(setExcPath(path));
|
|
172
|
-
|
|
173
|
-
.
|
|
174
|
-
|
|
192
|
+
try {
|
|
193
|
+
const link = await parentFolder.getLink(fileName)
|
|
194
|
+
.catch(setExcPath(path));
|
|
195
|
+
await parentFolder.removeChild(link);
|
|
196
|
+
}
|
|
197
|
+
catch (exc) {
|
|
198
|
+
if (exc.type === 'secondary') {
|
|
199
|
+
await parentFolder.removeChildEntryUnconditionally(fileName);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
throw exc;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
175
205
|
}
|
|
176
206
|
async move(initPath, newPath) {
|
|
177
207
|
const srcFolderPath = splitPathIntoParts(initPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-3nweb-client-lib",
|
|
3
|
-
"version": "0.45.
|
|
3
|
+
"version": "0.45.3",
|
|
4
4
|
"description": "3NWeb client core library, embeddable into different environments",
|
|
5
5
|
"main": "build/lib-index.js",
|
|
6
6
|
"types": "build/lib-index.d.ts",
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/jasmine": "^5.1.13",
|
|
47
|
-
"@types/node": "^
|
|
47
|
+
"@types/node": "^24.1.0",
|
|
48
48
|
"@types/punycode": "^2.1.1",
|
|
49
49
|
"@types/ws": "^8.18.1",
|
|
50
|
-
"napi-nacl": "^1.2.0",
|
|
51
50
|
"jasmine": "^5.13.0",
|
|
51
|
+
"napi-nacl": "^1.2.1",
|
|
52
52
|
"protobufjs-cli": "^1.0.2",
|
|
53
53
|
"spec-3nweb-server": "^1.8.5",
|
|
54
54
|
"tsuml2": "^0.17.1",
|