@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.
Files changed (100) hide show
  1. package/dist/blob-api.d.ts.map +1 -1
  2. package/dist/blob-store/downloader.d.ts.map +1 -1
  3. package/dist/blob-store/hyperdrive-index.d.ts.map +1 -1
  4. package/dist/blob-store/index.d.ts.map +1 -1
  5. package/dist/core-manager/bitfield-rle.d.ts.map +1 -1
  6. package/dist/core-manager/core-index.d.ts.map +1 -1
  7. package/dist/core-manager/index.d.ts +1 -2
  8. package/dist/core-manager/index.d.ts.map +1 -1
  9. package/dist/core-ownership.d.ts.map +1 -1
  10. package/dist/datastore/index.d.ts.map +1 -1
  11. package/dist/datatype/index.d.ts +7 -0
  12. package/dist/datatype/index.d.ts.map +1 -1
  13. package/dist/discovery/local-discovery.d.ts.map +1 -1
  14. package/dist/errors.d.ts +437 -35
  15. package/dist/errors.d.ts.map +1 -1
  16. package/dist/fastify-plugins/blobs.d.ts.map +1 -1
  17. package/dist/fastify-plugins/icons.d.ts.map +1 -1
  18. package/dist/fastify-plugins/maps.d.ts.map +1 -1
  19. package/dist/generated/extensions.d.ts +1 -1
  20. package/dist/generated/extensions.d.ts.map +1 -1
  21. package/dist/generated/rpc.d.ts +1 -0
  22. package/dist/generated/rpc.d.ts.map +1 -1
  23. package/dist/icon-api.d.ts +0 -1
  24. package/dist/icon-api.d.ts.map +1 -1
  25. package/dist/import-categories.d.ts.map +1 -1
  26. package/dist/index-writer/index.d.ts.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/intl/parse-bcp-47.d.ts.map +1 -1
  30. package/dist/invite/invite-api.d.ts.map +1 -1
  31. package/dist/lib/drizzle-helpers.d.ts.map +1 -1
  32. package/dist/lib/hypercore-helpers.d.ts.map +1 -1
  33. package/dist/lib/key-by.d.ts.map +1 -1
  34. package/dist/local-peers.d.ts +0 -14
  35. package/dist/local-peers.d.ts.map +1 -1
  36. package/dist/logger.d.ts.map +1 -1
  37. package/dist/mapeo-manager.d.ts +2 -1
  38. package/dist/mapeo-manager.d.ts.map +1 -1
  39. package/dist/mapeo-project.d.ts +15 -8
  40. package/dist/mapeo-project.d.ts.map +1 -1
  41. package/dist/member-api.d.ts +42 -7
  42. package/dist/member-api.d.ts.map +1 -1
  43. package/dist/roles.d.ts.map +1 -1
  44. package/dist/schema/json-schema-to-drizzle.d.ts.map +1 -1
  45. package/dist/schema.d.ts +2 -0
  46. package/dist/schema.d.ts.map +1 -0
  47. package/dist/sync/core-sync-state.d.ts.map +1 -1
  48. package/dist/sync/peer-sync-controller.d.ts.map +1 -1
  49. package/dist/sync/sync-api.d.ts.map +1 -1
  50. package/dist/utils.d.ts +8 -10
  51. package/dist/utils.d.ts.map +1 -1
  52. package/package.json +18 -2
  53. package/src/blob-api.js +24 -4
  54. package/src/blob-store/downloader.js +7 -6
  55. package/src/blob-store/entries-stream.js +1 -1
  56. package/src/blob-store/hyperdrive-index.js +3 -5
  57. package/src/blob-store/index.js +15 -20
  58. package/src/core-manager/bitfield-rle.js +2 -1
  59. package/src/core-manager/core-index.js +2 -1
  60. package/src/core-manager/index.js +12 -13
  61. package/src/core-ownership.js +7 -3
  62. package/src/datastore/index.js +13 -9
  63. package/src/datatype/index.js +28 -5
  64. package/src/discovery/local-discovery.js +8 -7
  65. package/src/errors.js +530 -62
  66. package/src/fastify-controller.js +3 -3
  67. package/src/fastify-plugins/blobs.js +21 -14
  68. package/src/fastify-plugins/icons.js +18 -9
  69. package/src/fastify-plugins/maps.js +6 -5
  70. package/src/generated/extensions.d.ts +1 -1
  71. package/src/generated/extensions.js +5 -5
  72. package/src/generated/extensions.ts +6 -6
  73. package/src/generated/rpc.d.ts +1 -0
  74. package/src/generated/rpc.js +12 -1
  75. package/src/generated/rpc.ts +13 -0
  76. package/src/icon-api.js +15 -7
  77. package/src/import-categories.js +6 -7
  78. package/src/index-writer/index.js +3 -2
  79. package/src/index.js +1 -0
  80. package/src/intl/parse-bcp-47.js +2 -1
  81. package/src/invite/invite-api.js +26 -20
  82. package/src/lib/drizzle-helpers.js +54 -39
  83. package/src/lib/hypercore-helpers.js +4 -2
  84. package/src/lib/key-by.js +3 -1
  85. package/src/local-peers.js +39 -46
  86. package/src/logger.js +2 -1
  87. package/src/mapeo-manager.js +36 -23
  88. package/src/mapeo-project.js +96 -67
  89. package/src/member-api.js +177 -96
  90. package/src/roles.js +11 -10
  91. package/src/schema/json-schema-to-drizzle.js +13 -4
  92. package/src/schema.js +1 -0
  93. package/src/sync/core-sync-state.js +2 -1
  94. package/src/sync/peer-sync-controller.js +4 -3
  95. package/src/sync/sync-api.js +9 -9
  96. package/src/translation-api.js +2 -2
  97. package/src/utils.js +58 -43
  98. package/dist/lib/error.d.ts +0 -51
  99. package/dist/lib/error.d.ts.map +0 -1
  100. package/src/lib/error.js +0 -71
@@ -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 { DataType, kCreateWithDocId } from './datatype/index.js'
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 { NotFoundError, nullIfNotFound } from './errors.js'
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 { createWriteStream } from 'fs'
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<MapShareExtension, 'bounds' | 'mapShareUrls'> & AugmentedMapShareProperties} MapShare
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', (err) => {
463
+ this.#blobStore.on('error', (e) => {
448
464
  // Ignore hypercore inflight request cancellation
449
- if (ensureError(err).message.includes('REQUEST_CANCELLED')) return
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', err)
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
- assert(doc.schemaName === 'translation', 'expected a translation doc')
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 (e) {
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 (e) {
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 Error(
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
- ...mapShareBase,
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
- const existingDoc = await deviceInfo
881
- .getByDocId(configCoreId)
882
- .catch(nullIfNotFound)
883
- if (existingDoc) {
884
- return await deviceInfo.update(existingDoc.versionId, doc)
885
- } else {
886
- return await deviceInfo[kCreateWithDocId](configCoreId, doc)
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
- return filePath
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
- await pipelinePromise(source, sink)
1400
+ const sink = fs.createWriteStream(filePath)
1401
+ try {
1402
+ await pipelinePromise(source, sink)
1369
1403
 
1370
- return filePath
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
- assert(
1440
- !this.#importingCategories,
1441
- 'Cannot run multiple category imports at the same time'
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
- this.#l.log('error loading config', e)
1449
- throw e
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 {MapShareExtension} mapShare
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(mapShare)
1512
+ validateMapShareExtension(shareExtension)
1465
1513
  }
1466
- const peerId = Buffer.from(mapShare.receiverDeviceId, 'hex')
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}