@comapeo/core 5.4.1 → 6.0.0
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/dist/blob-api.d.ts.map +1 -1
- package/dist/blob-store/downloader.d.ts.map +1 -1
- package/dist/blob-store/hyperdrive-index.d.ts.map +1 -1
- package/dist/blob-store/index.d.ts.map +1 -1
- package/dist/core-manager/bitfield-rle.d.ts.map +1 -1
- package/dist/core-manager/core-index.d.ts.map +1 -1
- package/dist/core-manager/index.d.ts +1 -2
- package/dist/core-manager/index.d.ts.map +1 -1
- package/dist/core-ownership.d.ts.map +1 -1
- package/dist/datastore/index.d.ts.map +1 -1
- package/dist/datatype/index.d.ts +7 -0
- package/dist/datatype/index.d.ts.map +1 -1
- package/dist/discovery/local-discovery.d.ts.map +1 -1
- package/dist/errors.d.ts +437 -35
- package/dist/errors.d.ts.map +1 -1
- package/dist/fastify-plugins/blobs.d.ts.map +1 -1
- package/dist/fastify-plugins/icons.d.ts.map +1 -1
- package/dist/fastify-plugins/maps.d.ts.map +1 -1
- package/dist/generated/extensions.d.ts +1 -1
- package/dist/generated/extensions.d.ts.map +1 -1
- package/dist/generated/rpc.d.ts +1 -0
- package/dist/generated/rpc.d.ts.map +1 -1
- package/dist/icon-api.d.ts +0 -1
- package/dist/icon-api.d.ts.map +1 -1
- package/dist/import-categories.d.ts.map +1 -1
- package/dist/index-writer/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/intl/parse-bcp-47.d.ts.map +1 -1
- package/dist/invite/invite-api.d.ts.map +1 -1
- package/dist/lib/drizzle-helpers.d.ts.map +1 -1
- package/dist/lib/hypercore-helpers.d.ts.map +1 -1
- package/dist/lib/key-by.d.ts.map +1 -1
- package/dist/local-peers.d.ts +0 -14
- package/dist/local-peers.d.ts.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/mapeo-manager.d.ts +2 -1
- package/dist/mapeo-manager.d.ts.map +1 -1
- package/dist/mapeo-project.d.ts +15 -8
- package/dist/mapeo-project.d.ts.map +1 -1
- package/dist/member-api.d.ts +42 -7
- package/dist/member-api.d.ts.map +1 -1
- package/dist/roles.d.ts.map +1 -1
- package/dist/schema/json-schema-to-drizzle.d.ts.map +1 -1
- package/dist/schema.d.ts +2 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/sync/core-sync-state.d.ts.map +1 -1
- package/dist/sync/peer-sync-controller.d.ts.map +1 -1
- package/dist/sync/sync-api.d.ts.map +1 -1
- package/dist/utils.d.ts +8 -10
- package/dist/utils.d.ts.map +1 -1
- package/package.json +18 -2
- package/src/blob-api.js +24 -4
- package/src/blob-store/downloader.js +7 -6
- package/src/blob-store/entries-stream.js +1 -1
- package/src/blob-store/hyperdrive-index.js +3 -5
- package/src/blob-store/index.js +15 -20
- package/src/core-manager/bitfield-rle.js +2 -1
- package/src/core-manager/core-index.js +2 -1
- package/src/core-manager/index.js +12 -13
- package/src/core-ownership.js +7 -3
- package/src/datastore/index.js +13 -9
- package/src/datatype/index.js +28 -5
- package/src/discovery/local-discovery.js +8 -7
- package/src/errors.js +530 -62
- package/src/fastify-controller.js +3 -3
- package/src/fastify-plugins/blobs.js +21 -14
- package/src/fastify-plugins/icons.js +18 -9
- package/src/fastify-plugins/maps.js +6 -5
- package/src/generated/extensions.d.ts +1 -1
- package/src/generated/extensions.js +5 -5
- package/src/generated/extensions.ts +6 -6
- package/src/generated/rpc.d.ts +1 -0
- package/src/generated/rpc.js +12 -1
- package/src/generated/rpc.ts +13 -0
- package/src/icon-api.js +15 -7
- package/src/import-categories.js +6 -7
- package/src/index-writer/index.js +3 -2
- package/src/index.js +1 -0
- package/src/intl/parse-bcp-47.js +2 -1
- package/src/invite/invite-api.js +26 -20
- package/src/lib/drizzle-helpers.js +54 -39
- package/src/lib/hypercore-helpers.js +4 -2
- package/src/lib/key-by.js +3 -1
- package/src/local-peers.js +39 -46
- package/src/logger.js +2 -1
- package/src/mapeo-manager.js +36 -23
- package/src/mapeo-project.js +96 -67
- package/src/member-api.js +177 -96
- package/src/roles.js +11 -10
- package/src/schema/json-schema-to-drizzle.js +13 -4
- package/src/schema.js +1 -0
- package/src/sync/core-sync-state.js +2 -1
- package/src/sync/peer-sync-controller.js +4 -3
- package/src/sync/sync-api.js +9 -9
- package/src/translation-api.js +2 -2
- package/src/utils.js +58 -43
- package/dist/lib/error.d.ts +0 -51
- package/dist/lib/error.d.ts.map +0 -1
- package/src/lib/error.js +0 -71
package/src/mapeo-project.js
CHANGED
|
@@ -3,7 +3,6 @@ import Database from 'better-sqlite3'
|
|
|
3
3
|
import { decodeBlockPrefix, decode, parseVersionId } from '@comapeo/schema'
|
|
4
4
|
import { drizzle } from 'drizzle-orm/better-sqlite3'
|
|
5
5
|
import { sql, count, eq } from 'drizzle-orm'
|
|
6
|
-
import { discoveryKey } from 'hypercore-crypto'
|
|
7
6
|
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
8
7
|
import ZipArchive from 'zip-stream-promise'
|
|
9
8
|
import * as b4a from 'b4a'
|
|
@@ -14,7 +13,11 @@ import { Readable, pipelinePromise } from 'streamx'
|
|
|
14
13
|
import { NAMESPACES, NAMESPACE_SCHEMAS } from './constants.js'
|
|
15
14
|
import { CoreManager } from './core-manager/index.js'
|
|
16
15
|
import { DataStore } from './datastore/index.js'
|
|
17
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
DataType,
|
|
18
|
+
kCreateOrUpdateWithDocId,
|
|
19
|
+
kCreateWithDocId,
|
|
20
|
+
} from './datatype/index.js'
|
|
18
21
|
import { BlobStore } from './blob-store/index.js'
|
|
19
22
|
import { BlobApi } from './blob-api.js'
|
|
20
23
|
import { IndexWriter } from './index-writer/index.js'
|
|
@@ -43,9 +46,7 @@ import {
|
|
|
43
46
|
INACTIVE_MEMBER_ROLE_IDS,
|
|
44
47
|
} from './roles.js'
|
|
45
48
|
import {
|
|
46
|
-
assert,
|
|
47
49
|
buildBlobId,
|
|
48
|
-
ExhaustivenessError,
|
|
49
50
|
getDeviceId,
|
|
50
51
|
projectKeyToId,
|
|
51
52
|
projectKeyToPublicId,
|
|
@@ -64,9 +65,21 @@ import { Logger } from './logger.js'
|
|
|
64
65
|
import { IconApi } from './icon-api.js'
|
|
65
66
|
import { importCategories } from './import-categories.js'
|
|
66
67
|
import TranslationApi from './translation-api.js'
|
|
67
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
CategoryFileNotFoundError,
|
|
70
|
+
ensureKnownError,
|
|
71
|
+
getErrorCode,
|
|
72
|
+
NotFoundError,
|
|
73
|
+
ExhaustivenessError,
|
|
74
|
+
nullIfNotFound,
|
|
75
|
+
GeoJSONExportError,
|
|
76
|
+
InvalidMapShareError,
|
|
77
|
+
MultipleCategoryImportsError,
|
|
78
|
+
UnexpectedDocSchemaError,
|
|
79
|
+
} from './errors.js'
|
|
68
80
|
import { WebSocket } from 'ws'
|
|
69
|
-
import
|
|
81
|
+
import fs from 'node:fs'
|
|
82
|
+
|
|
70
83
|
import ensureError from 'ensure-error'
|
|
71
84
|
/** @import { MapShareExtension } from './generated/extensions.js' */
|
|
72
85
|
/** @import { ProjectSettingsValue, Observation, Track } from '@comapeo/schema' */
|
|
@@ -123,10 +136,15 @@ const VARIANT_EXPORT_ORDER = ['original', 'preview', 'thumbnail']
|
|
|
123
136
|
* @property {number} mapShareReceivedAt - Timestamp when the map share was received.
|
|
124
137
|
* @property {string} senderDeviceId - The ID of the device that sent the map share.
|
|
125
138
|
* @property {string} [senderDeviceName] - The name of the device that sent the map share.
|
|
139
|
+
* @property {string} receiverDeviceId - The deviceId of the peer the map share was sent to
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @typedef {Omit<MapShareExtension, 'bounds' | 'mapShareUrls' | 'receiverDeviceKey'> & AugmentedMapShareProperties} MapShare
|
|
126
144
|
*/
|
|
127
145
|
|
|
128
146
|
/**
|
|
129
|
-
* @typedef {Omit<
|
|
147
|
+
* @typedef {Omit<MapShare, 'mapShareReceivedAt' | 'senderDeviceId' | 'senderDeviceName'>} MapShareSend
|
|
130
148
|
*/
|
|
131
149
|
|
|
132
150
|
/**
|
|
@@ -230,7 +248,7 @@ export class MapeoProject extends TypedEmitter {
|
|
|
230
248
|
reindex = true
|
|
231
249
|
break
|
|
232
250
|
default:
|
|
233
|
-
throw new ExhaustivenessError(migrationResult)
|
|
251
|
+
throw new ExhaustivenessError({ value: migrationResult })
|
|
234
252
|
}
|
|
235
253
|
|
|
236
254
|
const indexedTables = [
|
|
@@ -287,8 +305,6 @@ export class MapeoProject extends TypedEmitter {
|
|
|
287
305
|
switch (doc.schemaName) {
|
|
288
306
|
case 'coreOwnership':
|
|
289
307
|
return mapAndValidateCoreOwnership(doc, version)
|
|
290
|
-
case 'deviceInfo':
|
|
291
|
-
return mapAndValidateDeviceInfo(doc, version)
|
|
292
308
|
default:
|
|
293
309
|
return doc
|
|
294
310
|
}
|
|
@@ -444,12 +460,12 @@ export class MapeoProject extends TypedEmitter {
|
|
|
444
460
|
logger: this.#l,
|
|
445
461
|
})
|
|
446
462
|
|
|
447
|
-
this.#blobStore.on('error', (
|
|
463
|
+
this.#blobStore.on('error', (e) => {
|
|
448
464
|
// Ignore hypercore inflight request cancellation
|
|
449
|
-
if (ensureError(
|
|
465
|
+
if (ensureError(e).message.includes('REQUEST_CANCELLED')) return
|
|
450
466
|
// TODO: Handle this error in some way - this error will come from an
|
|
451
467
|
// unexpected error with background blob downloads
|
|
452
|
-
console.error('BlobStore error',
|
|
468
|
+
console.error('BlobStore error', e)
|
|
453
469
|
})
|
|
454
470
|
|
|
455
471
|
this.$blobs = new BlobApi({
|
|
@@ -653,7 +669,13 @@ export class MapeoProject extends TypedEmitter {
|
|
|
653
669
|
index: entry.index,
|
|
654
670
|
})
|
|
655
671
|
|
|
656
|
-
|
|
672
|
+
if (doc.schemaName !== 'translation') {
|
|
673
|
+
throw new UnexpectedDocSchemaError({
|
|
674
|
+
gotSchema: doc.schemaName,
|
|
675
|
+
expectedSchema: 'translation',
|
|
676
|
+
})
|
|
677
|
+
}
|
|
678
|
+
|
|
657
679
|
this.#translationApi.index(doc)
|
|
658
680
|
otherEntries.push(entry)
|
|
659
681
|
} else {
|
|
@@ -739,7 +761,7 @@ export class MapeoProject extends TypedEmitter {
|
|
|
739
761
|
return extractEditableProjectSettings(
|
|
740
762
|
await this.#dataTypes.projectSettings.getByDocId(this.#projectId)
|
|
741
763
|
)
|
|
742
|
-
} catch
|
|
764
|
+
} catch {
|
|
743
765
|
// if (e instanceof Error && e.name !== 'NotFoundError') throw e
|
|
744
766
|
// If the project has not completed an initial sync, project settings will
|
|
745
767
|
// not be available, so use fallback project info which is set from the
|
|
@@ -757,7 +779,7 @@ export class MapeoProject extends TypedEmitter {
|
|
|
757
779
|
// Should error if we haven't synced before
|
|
758
780
|
await this.#dataTypes.projectSettings.getByDocId(this.#projectId)
|
|
759
781
|
return true
|
|
760
|
-
} catch
|
|
782
|
+
} catch {
|
|
761
783
|
return false
|
|
762
784
|
}
|
|
763
785
|
}
|
|
@@ -795,17 +817,26 @@ export class MapeoProject extends TypedEmitter {
|
|
|
795
817
|
const sender = await this.$member.getById(senderDeviceId)
|
|
796
818
|
|
|
797
819
|
if (INACTIVE_MEMBER_ROLE_IDS.includes(sender.role.roleId)) {
|
|
798
|
-
throw new
|
|
820
|
+
throw new InvalidMapShareError(
|
|
799
821
|
`Map Share Sender is not an active member of the project (role: ${sender.role.name})`
|
|
800
822
|
)
|
|
801
823
|
}
|
|
802
824
|
|
|
825
|
+
const { receiverDeviceKey, ...mapShareData } = mapShareBase
|
|
826
|
+
|
|
827
|
+
const receiverDeviceId = receiverDeviceKey.toString('hex')
|
|
828
|
+
|
|
829
|
+
if (receiverDeviceId !== this.#deviceId) {
|
|
830
|
+
throw new Error('Got map share intended for a different peer')
|
|
831
|
+
}
|
|
832
|
+
|
|
803
833
|
/** @type {MapShare} */
|
|
804
834
|
const mapShare = {
|
|
805
|
-
...
|
|
835
|
+
...mapShareData,
|
|
806
836
|
senderDeviceId,
|
|
807
837
|
senderDeviceName: sender.name,
|
|
808
838
|
mapShareReceivedAt,
|
|
839
|
+
receiverDeviceId,
|
|
809
840
|
}
|
|
810
841
|
|
|
811
842
|
this.emit('map-share', mapShare)
|
|
@@ -861,15 +892,10 @@ export class MapeoProject extends TypedEmitter {
|
|
|
861
892
|
|
|
862
893
|
/**
|
|
863
894
|
* @param {Pick<import('@comapeo/schema').DeviceInfoValue, 'name' | 'deviceType' | 'selfHostedServerDetails'>} value
|
|
864
|
-
* @returns {Promise<import('@comapeo/schema').DeviceInfo>}
|
|
865
895
|
*/
|
|
866
896
|
async [kSetOwnDeviceInfo](value) {
|
|
867
897
|
const { deviceInfo } = this.#dataTypes
|
|
868
898
|
|
|
869
|
-
const configCoreId = this.#coreManager
|
|
870
|
-
.getWriterCore('config')
|
|
871
|
-
.key.toString('hex')
|
|
872
|
-
|
|
873
899
|
const doc = {
|
|
874
900
|
name: value.name,
|
|
875
901
|
deviceType: value.deviceType,
|
|
@@ -877,13 +903,15 @@ export class MapeoProject extends TypedEmitter {
|
|
|
877
903
|
schemaName: /** @type {const} */ ('deviceInfo'),
|
|
878
904
|
}
|
|
879
905
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
.
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
906
|
+
// TODO: Remove configCore once we know everyone has deviceId
|
|
907
|
+
const configCoreId = this.#coreManager
|
|
908
|
+
.getWriterCore('config')
|
|
909
|
+
.key.toString('hex')
|
|
910
|
+
|
|
911
|
+
const docIds = [this.deviceId, configCoreId]
|
|
912
|
+
|
|
913
|
+
for (const docId of docIds) {
|
|
914
|
+
await deviceInfo[kCreateOrUpdateWithDocId](docId, doc)
|
|
887
915
|
}
|
|
888
916
|
}
|
|
889
917
|
|
|
@@ -1191,10 +1219,16 @@ export class MapeoProject extends TypedEmitter {
|
|
|
1191
1219
|
const fileName = await this[kGeoJSONFileName](observations, tracks)
|
|
1192
1220
|
const filePath = path.join(exportFolder, fileName)
|
|
1193
1221
|
const source = this.#exportGeoJSONStream({ observations, tracks, lang })
|
|
1194
|
-
const sink = createWriteStream(filePath)
|
|
1195
|
-
await pipelinePromise(source, sink)
|
|
1196
1222
|
|
|
1197
|
-
|
|
1223
|
+
const sink = fs.createWriteStream(filePath)
|
|
1224
|
+
|
|
1225
|
+
try {
|
|
1226
|
+
await pipelinePromise(source, sink)
|
|
1227
|
+
|
|
1228
|
+
return filePath
|
|
1229
|
+
} catch (e) {
|
|
1230
|
+
throw new GeoJSONExportError({ cause: e })
|
|
1231
|
+
}
|
|
1198
1232
|
}
|
|
1199
1233
|
|
|
1200
1234
|
/**
|
|
@@ -1228,12 +1262,11 @@ export class MapeoProject extends TypedEmitter {
|
|
|
1228
1262
|
}
|
|
1229
1263
|
return { blobId, mimeType }
|
|
1230
1264
|
} catch (e) {
|
|
1231
|
-
if (!(e instanceof Error)) throw e
|
|
1232
1265
|
this.#l.log(
|
|
1233
1266
|
'Error loading blob id for attachment',
|
|
1234
1267
|
attachment,
|
|
1235
1268
|
variant,
|
|
1236
|
-
e.message
|
|
1269
|
+
ensureError(e).message
|
|
1237
1270
|
)
|
|
1238
1271
|
continue
|
|
1239
1272
|
}
|
|
@@ -1336,7 +1369,7 @@ export class MapeoProject extends TypedEmitter {
|
|
|
1336
1369
|
tracks,
|
|
1337
1370
|
attachments,
|
|
1338
1371
|
lang,
|
|
1339
|
-
}).catch((e) => archive.emit('error', e))
|
|
1372
|
+
}).catch((e) => archive.emit('error', ensureError(e)))
|
|
1340
1373
|
|
|
1341
1374
|
// @ts-expect-error
|
|
1342
1375
|
return archive
|
|
@@ -1364,10 +1397,14 @@ export class MapeoProject extends TypedEmitter {
|
|
|
1364
1397
|
attachments,
|
|
1365
1398
|
lang,
|
|
1366
1399
|
})
|
|
1367
|
-
const sink = createWriteStream(filePath)
|
|
1368
|
-
|
|
1400
|
+
const sink = fs.createWriteStream(filePath)
|
|
1401
|
+
try {
|
|
1402
|
+
await pipelinePromise(source, sink)
|
|
1369
1403
|
|
|
1370
|
-
|
|
1404
|
+
return filePath
|
|
1405
|
+
} catch (e) {
|
|
1406
|
+
throw new GeoJSONExportError({ cause: e })
|
|
1407
|
+
}
|
|
1371
1408
|
}
|
|
1372
1409
|
|
|
1373
1410
|
async [kProjectLeave]() {
|
|
@@ -1436,17 +1473,19 @@ export class MapeoProject extends TypedEmitter {
|
|
|
1436
1473
|
* @returns {Promise<void>}
|
|
1437
1474
|
*/
|
|
1438
1475
|
async $importCategories({ filePath }) {
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
)
|
|
1476
|
+
if (this.#importingCategories) {
|
|
1477
|
+
throw new MultipleCategoryImportsError()
|
|
1478
|
+
}
|
|
1443
1479
|
this.#importingCategories = true
|
|
1444
1480
|
|
|
1445
1481
|
try {
|
|
1446
1482
|
await importCategories(this, { filePath, logger: this.#l })
|
|
1447
1483
|
} catch (e) {
|
|
1448
|
-
|
|
1449
|
-
|
|
1484
|
+
if (getErrorCode(e) === 'ENOENT') {
|
|
1485
|
+
throw new CategoryFileNotFoundError({ filePath })
|
|
1486
|
+
}
|
|
1487
|
+
this.#l.log('ERROR: could not load config', e)
|
|
1488
|
+
throw ensureKnownError(e)
|
|
1450
1489
|
} finally {
|
|
1451
1490
|
this.#importingCategories = false
|
|
1452
1491
|
}
|
|
@@ -1455,16 +1494,24 @@ export class MapeoProject extends TypedEmitter {
|
|
|
1455
1494
|
/**
|
|
1456
1495
|
* Send a map share offer to the peer with device ID `mapShare.receiverDeviceId`
|
|
1457
1496
|
*
|
|
1458
|
-
* @param {
|
|
1497
|
+
* @param {MapShareSend} mapShare
|
|
1459
1498
|
* @param {object} [options]
|
|
1460
1499
|
* @param {boolean} [options.__testOnlyBypassValidation=false] Warning: Do not use!
|
|
1461
1500
|
*/
|
|
1462
1501
|
async $sendMapShare(mapShare, { __testOnlyBypassValidation = false } = {}) {
|
|
1502
|
+
const { receiverDeviceId, ...mapShareData } = mapShare
|
|
1503
|
+
const receiverDeviceKey = Buffer.from(receiverDeviceId, 'hex')
|
|
1504
|
+
|
|
1505
|
+
/** @type {MapShareExtension} */
|
|
1506
|
+
// @ts-expect-error readonly fields being assigned as mutable
|
|
1507
|
+
const shareExtension = {
|
|
1508
|
+
...mapShareData,
|
|
1509
|
+
receiverDeviceKey,
|
|
1510
|
+
}
|
|
1463
1511
|
if (!__testOnlyBypassValidation) {
|
|
1464
|
-
validateMapShareExtension(
|
|
1512
|
+
validateMapShareExtension(shareExtension)
|
|
1465
1513
|
}
|
|
1466
|
-
|
|
1467
|
-
await this.#coreManager.sendMapShare(mapShare, peerId)
|
|
1514
|
+
await this.#coreManager.sendMapShare(shareExtension)
|
|
1468
1515
|
}
|
|
1469
1516
|
}
|
|
1470
1517
|
|
|
@@ -1503,24 +1550,6 @@ function getCoreKeypairs({ projectKey, projectSecretKey, keyManager }) {
|
|
|
1503
1550
|
}
|
|
1504
1551
|
|
|
1505
1552
|
/**
|
|
1506
|
-
* Validate that a deviceInfo record is written by the device that is it about,
|
|
1507
|
-
* e.g. version.coreKey should equal docId
|
|
1508
|
-
*
|
|
1509
|
-
* @param {import('@comapeo/schema').DeviceInfo} doc
|
|
1510
|
-
* @param {import('@comapeo/schema').VersionIdObject} version
|
|
1511
|
-
* @returns {import('@comapeo/schema').DeviceInfo}
|
|
1512
|
-
*/
|
|
1513
|
-
function mapAndValidateDeviceInfo(doc, { coreDiscoveryKey }) {
|
|
1514
|
-
if (!coreDiscoveryKey.equals(discoveryKey(Buffer.from(doc.docId, 'hex')))) {
|
|
1515
|
-
throw new Error(
|
|
1516
|
-
'Invalid deviceInfo record, cannot write deviceInfo for another device'
|
|
1517
|
-
)
|
|
1518
|
-
}
|
|
1519
|
-
return doc
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
/**
|
|
1523
|
-
*
|
|
1524
1553
|
* @param {string} baseUrl
|
|
1525
1554
|
* @param {string} projectPublicId
|
|
1526
1555
|
* @returns {string}
|