@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
@@ -1,6 +1,6 @@
1
1
  import { TypedEmitter } from 'tiny-typed-emitter'
2
2
  import { InviteResponse_Decision } from '../generated/rpc.js'
3
- import { assert, keyToId, noop } from '../utils.js'
3
+ import { keyToId, noop } from '../utils.js'
4
4
  import HashMap from '../lib/hashmap.js'
5
5
  import timingSafeEqual from 'string-timing-safe-equal'
6
6
  import { Logger } from '../logger.js'
@@ -10,6 +10,8 @@ import {
10
10
  NotFoundError,
11
11
  AlreadyJoinedError,
12
12
  InviteSendError,
13
+ ensureKnownError,
14
+ InviteNotFoundError,
13
15
  } from '../errors.js'
14
16
 
15
17
  /** @import { ProjectToAddDetails } from '../mapeo-manager.js' */
@@ -79,24 +81,24 @@ export class InviteApi extends TypedEmitter {
79
81
  this.rpc.on('invite', (...args) => {
80
82
  try {
81
83
  this.#handleNewInvite(...args)
82
- } catch (err) {
83
- console.error('Error handling invite', err)
84
+ } catch (e) {
85
+ this.#l.log('ERROR: could not handle invite', e)
84
86
  }
85
87
  })
86
88
 
87
89
  this.rpc.on('invite-cancel', (_peerId, inviteCancel) => {
88
90
  try {
89
91
  this.#handleInviteCancel(inviteCancel)
90
- } catch (err) {
91
- console.error('Error handling invite cancel', err)
92
+ } catch (e) {
93
+ this.#l.log('ERROR: could not handle invite cancel', e)
92
94
  }
93
95
  })
94
96
 
95
97
  this.rpc.on('got-project-details', (peerId, projectJoinDetails) => {
96
98
  try {
97
99
  this.#handleGotProjectDetails(peerId, projectJoinDetails)
98
- } catch (err) {
99
- console.error('Error handling got-project-details', err)
100
+ } catch (e) {
101
+ this.#l.log('ERROR: could not handle got-project-details', e)
100
102
  }
101
103
  })
102
104
  }
@@ -127,6 +129,7 @@ export class InviteApi extends TypedEmitter {
127
129
  projectColor,
128
130
  projectDescription,
129
131
  sendStats,
132
+ invitorWroteDeviceInfo,
130
133
  } = inviteRpcMessage
131
134
  const invite = { ...inviteRpcMessage, receivedAt: Date.now() }
132
135
 
@@ -167,6 +170,7 @@ export class InviteApi extends TypedEmitter {
167
170
  projectColor,
168
171
  projectDescription,
169
172
  sendStats,
173
+ invitorWroteDeviceInfo,
170
174
  })
171
175
  }),
172
176
  },
@@ -202,7 +206,9 @@ export class InviteApi extends TypedEmitter {
202
206
  this.#l.log('Received invite cancel for invite ID %h', inviteId)
203
207
 
204
208
  const invite = this.#invites.get(inviteId)
205
- assert(!!invite, `Cannot find invite ${inviteId.toString('hex')}`)
209
+ if (!invite) {
210
+ throw new InviteNotFoundError({ inviteId: inviteId.toString('hex') })
211
+ }
206
212
 
207
213
  // TODO: Move this logging to the state machine
208
214
  const state = invite.actor.getSnapshot()
@@ -292,10 +298,9 @@ export class InviteApi extends TypedEmitter {
292
298
  const inviteId = Buffer.from(inviteIdString, 'hex')
293
299
 
294
300
  const invite = this.#invites.get(inviteId)
295
- assert(
296
- !!invite,
297
- new NotFoundError(`Cannot find invite ${inviteIdString}`)
298
- )
301
+ if (!invite) {
302
+ throw new InviteNotFoundError({ inviteId: inviteId.toString('hex') })
303
+ }
299
304
  assertCanSend(invite.actor, { type: 'ACCEPT_INVITE' })
300
305
 
301
306
  this.#l.log('Accepting invite %h', inviteId)
@@ -321,9 +326,9 @@ export class InviteApi extends TypedEmitter {
321
326
  }
322
327
 
323
328
  return projectPublicId
324
- } catch (err) {
325
- this.#l.log('ERROR: Unable to accept invite', err)
326
- throw err
329
+ } catch (e) {
330
+ this.#l.log('ERROR: Unable to accept invite', e)
331
+ throw ensureKnownError(e)
327
332
  }
328
333
  }
329
334
 
@@ -335,7 +340,9 @@ export class InviteApi extends TypedEmitter {
335
340
  const inviteId = Buffer.from(inviteIdString, 'hex')
336
341
 
337
342
  const invite = this.#invites.get(inviteId)
338
- assert(!!invite, `Cannot find invite ${inviteIdString}`)
343
+ if (!invite) {
344
+ throw new InviteNotFoundError({ inviteId: inviteId.toString('hex') })
345
+ }
339
346
  assertCanSend(invite.actor, { type: 'REJECT_INVITE' })
340
347
 
341
348
  this.#l.log('Rejecting invite %h', inviteId)
@@ -381,14 +388,13 @@ function toInvite(internal, snapshot, invitorDeviceId) {
381
388
  */
382
389
  function assertCanSend(actor, eventType) {
383
390
  const state = actor.getSnapshot()
384
- assert(
385
- state.can(eventType),
386
- new InviteSendError(
391
+ if (!state.can(eventType)) {
392
+ throw new InviteSendError(
387
393
  `Cannot send ${JSON.stringify(eventType)} in state ${toStateString(
388
394
  state.value
389
395
  )}`
390
396
  )
391
- )
397
+ }
392
398
  }
393
399
 
394
400
  /**
@@ -1,9 +1,13 @@
1
1
  import { sql } from 'drizzle-orm'
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
- import { assert } from '../utils.js'
5
4
  import { migrate as drizzleMigrate } from 'drizzle-orm/better-sqlite3/migrator'
6
5
  import { DRIZZLE_MIGRATIONS_TABLE } from '../constants.js'
6
+ import {
7
+ InvalidDrizzleJournalError,
8
+ InvalidDrizzleQueryResultError,
9
+ MigrationError,
10
+ } from '../errors.js'
7
11
  /** @import { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3' */
8
12
 
9
13
  /**
@@ -11,13 +15,16 @@ import { DRIZZLE_MIGRATIONS_TABLE } from '../constants.js'
11
15
  * @returns {number}
12
16
  */
13
17
  const getNumberResult = (queryResult) => {
14
- assert(
15
- queryResult &&
18
+ if (
19
+ !(
20
+ queryResult &&
16
21
  typeof queryResult === 'object' &&
17
22
  'result' in queryResult &&
18
- typeof queryResult.result === 'number',
19
- 'expected query to return proper result'
20
- )
23
+ typeof queryResult.result === 'number'
24
+ )
25
+ ) {
26
+ throw new InvalidDrizzleQueryResultError()
27
+ }
21
28
  return queryResult.result
22
29
  }
23
30
 
@@ -68,37 +75,40 @@ const safeGetLatestMigrationMillis = (db) =>
68
75
  * @returns {MigrationResult}
69
76
  */
70
77
  export function migrate(db, { migrationsFolder, migrationFns = {} }) {
71
- const journal = /** @type {unknown} */ (
72
- JSON.parse(
73
- fs.readFileSync(
74
- path.join(migrationsFolder, 'meta/_journal.json'),
75
- 'utf-8'
78
+ try {
79
+ const journal = /** @type {unknown} */ (
80
+ JSON.parse(
81
+ fs.readFileSync(
82
+ path.join(migrationsFolder, 'meta/_journal.json'),
83
+ 'utf-8'
84
+ )
76
85
  )
77
86
  )
78
- )
79
- // Drizzle _could_ decide to change the journal format in the future, but this
80
- // assertion will ensure that tests fail if they do.
81
- assertValidJournal(journal)
82
-
83
- const prevMigrationMillis = safeGetLatestMigrationMillis(db)
87
+ // Drizzle _could_ decide to change the journal format in the future, but this
88
+ // assertion will ensure that tests fail if they do.
89
+ assertValidJournal(journal)
84
90
 
85
- drizzleMigrate(db, {
86
- migrationsFolder,
87
- migrationsTable: DRIZZLE_MIGRATIONS_TABLE,
88
- })
91
+ const prevMigrationMillis = safeGetLatestMigrationMillis(db)
89
92
 
90
- for (const entry of journal.entries) {
91
- if (entry.when <= prevMigrationMillis) continue
92
- const fn = migrationFns[entry.tag]
93
- if (fn) fn(db)
94
- }
93
+ drizzleMigrate(db, {
94
+ migrationsFolder,
95
+ migrationsTable: DRIZZLE_MIGRATIONS_TABLE,
96
+ })
95
97
 
96
- const lastMigrationMillis = safeGetLatestMigrationMillis(db)
98
+ for (const entry of journal.entries) {
99
+ if (entry.when <= prevMigrationMillis) continue
100
+ const fn = migrationFns[entry.tag]
101
+ if (fn) fn(db)
102
+ }
97
103
 
98
- if (lastMigrationMillis === prevMigrationMillis) return 'no migration'
104
+ const lastMigrationMillis = safeGetLatestMigrationMillis(db)
99
105
 
100
- if (prevMigrationMillis === 0) return 'initialized database'
106
+ if (lastMigrationMillis === prevMigrationMillis) return 'no migration'
101
107
 
108
+ if (prevMigrationMillis === 0) return 'initialized database'
109
+ } catch (e) {
110
+ throw new MigrationError({ cause: e })
111
+ }
102
112
  return 'migrated'
103
113
  }
104
114
 
@@ -108,13 +118,16 @@ export function migrate(db, { migrationsFolder, migrationFns = {} }) {
108
118
  * @returns {asserts journal is { version: '5', entries: { tag: string, when: number }[] }}
109
119
  */
110
120
  function assertValidJournal(journal) {
111
- assert(journal && typeof journal === 'object', 'invalid journal')
112
- assert(
113
- 'version' in journal && journal.version === '5',
114
- 'unexpected journal version'
115
- )
116
- assert(
117
- 'entries' in journal &&
121
+ if (!(journal && typeof journal === 'object')) {
122
+ throw new InvalidDrizzleJournalError()
123
+ }
124
+ if (!('version' in journal && journal.version === '5')) {
125
+ throw new InvalidDrizzleJournalError('unexpected journal version')
126
+ }
127
+
128
+ if (
129
+ !(
130
+ 'entries' in journal &&
118
131
  Array.isArray(journal.entries) &&
119
132
  journal.entries.every(
120
133
  /** @param {unknown} m */
@@ -125,7 +138,9 @@ function assertValidJournal(journal) {
125
138
  typeof m.tag === 'string' &&
126
139
  'when' in m &&
127
140
  typeof m.when === 'number'
128
- ),
129
- 'invalid entries in journal'
130
- )
141
+ )
142
+ )
143
+ ) {
144
+ throw new InvalidDrizzleJournalError('invalid entries in journal')
145
+ }
131
146
  }
@@ -1,11 +1,13 @@
1
- import { assert } from '../utils.js'
1
+ import { MissingDiscoveryKeyError } from '../errors.js'
2
2
 
3
3
  /**
4
4
  * @param {import('hypercore')<'binary', any>} core Core to unreplicate. Must be ready.
5
5
  * @param {import('protomux')} protomux
6
6
  */
7
7
  export function unreplicate(core, protomux) {
8
- assert(core.discoveryKey, 'Core should have a discovery key')
8
+ if (!core.discoveryKey) {
9
+ throw new MissingDiscoveryKeyError()
10
+ }
9
11
  protomux.unpair({
10
12
  protocol: 'hypercore/alpha',
11
13
  id: core.discoveryKey,
package/src/lib/key-by.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { DuplicateKeyError } from '../errors.js'
2
+
1
3
  /**
2
4
  * Like [`Map.groupBy`][0], but the result's values aren't arrays.
3
5
  *
@@ -16,7 +18,7 @@ export function keyBy(items, callbackFn) {
16
18
  for (const item of items) {
17
19
  const key = callbackFn(item)
18
20
  if (result.has(key)) {
19
- throw new Error(`keyBy found duplicate key ${JSON.stringify(key)}`)
21
+ throw new DuplicateKeyError({ key: `${key}` })
20
22
  }
21
23
  result.set(key, item)
22
24
  }
@@ -1,7 +1,7 @@
1
1
  import { TypedEmitter } from 'tiny-typed-emitter'
2
2
  import Protomux from 'protomux'
3
3
  import timingSafeEqual from 'string-timing-safe-equal'
4
- import { assert, ExhaustivenessError, keyToId, noop } from './utils.js'
4
+ import { keyToId, noop, timeoutPromise } from './utils.js'
5
5
  import { isBlank } from './lib/string.js'
6
6
  import cenc from 'compact-encoding'
7
7
  import {
@@ -18,10 +18,15 @@ import {
18
18
  } from './generated/rpc.js'
19
19
  import pDefer from 'p-defer'
20
20
  import { Logger } from './logger.js'
21
- import pTimeout, { TimeoutError } from 'p-timeout'
22
21
  import {
22
+ PeerDisconnectedError,
23
+ PeerFailedConnectionError,
23
24
  RPCDisconnectBeforeAckError,
24
25
  RPCDisconnectBeforeSendingError,
26
+ UnknownPeerError,
27
+ ExhaustivenessError,
28
+ InvalidInviteError,
29
+ InvalidProjectJoinDetailsError,
25
30
  } from './errors.js'
26
31
  /** @import NoiseStream from '@hyperswarm/secret-stream' */
27
32
  /** @import { OpenedNoiseStream } from './lib/noise-secret-stream-helpers.js' */
@@ -157,7 +162,7 @@ class Peer {
157
162
  }
158
163
  /* c8 ignore next 2 */
159
164
  default:
160
- throw new ExhaustivenessError(this.#state)
165
+ throw new ExhaustivenessError({ value: this.#state })
161
166
  }
162
167
  }
163
168
  /**
@@ -280,7 +285,7 @@ class Peer {
280
285
  * @returns {Promise<void>}
281
286
  */
282
287
  async [kTestOnlySendRawInvite](buf) {
283
- this.#assertConnected()
288
+ this.#assertConnected('Peer not connected for test')
284
289
  const messageType = MESSAGE_TYPES.Invite
285
290
  await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
286
291
  }
@@ -408,7 +413,7 @@ class Peer {
408
413
  this.#features = deviceInfo.features
409
414
  this.#log('received deviceInfo %o', deviceInfo)
410
415
  }
411
- /** @param {string} [message] */
416
+ /** @param {string} message */
412
417
  #assertConnected(message) {
413
418
  if (this.#state === 'connected' && !this.#channel.closed) return
414
419
  /* c8 ignore next */
@@ -536,7 +541,6 @@ export class LocalPeers extends TypedEmitter {
536
541
  */
537
542
  connect(stream) {
538
543
  const noiseStream = stream.noiseStream
539
- if (!noiseStream) throw new Error('Invalid stream')
540
544
  const outerStream = noiseStream.rawStream
541
545
  const protomux =
542
546
  noiseStream.userData && Protomux.isProtomux(noiseStream.userData)
@@ -617,8 +621,8 @@ export class LocalPeers extends TypedEmitter {
617
621
  /** @type {keyof typeof MESSAGE_TYPES} */ (type),
618
622
  message
619
623
  )
620
- } catch (err) {
621
- const errorMessage = String(err)
624
+ } catch (e) {
625
+ const errorMessage = String(e)
622
626
  this.emit('failed-to-handle-message', type, errorMessage)
623
627
  this.#l.log(`Error handling ${type} message: ${errorMessage}`)
624
628
  }
@@ -817,7 +821,9 @@ export class LocalPeers extends TypedEmitter {
817
821
  * Wait for any connections that are currently opening
818
822
  */
819
823
  #waitForPendingConnections() {
820
- return pTimeout(Promise.all(this.#opening), { milliseconds: SEND_TIMEOUT })
824
+ return timeoutPromise(Promise.all(this.#opening), {
825
+ milliseconds: SEND_TIMEOUT,
826
+ })
821
827
  }
822
828
 
823
829
  /**
@@ -830,14 +836,14 @@ export class LocalPeers extends TypedEmitter {
830
836
  async #getPeerByDeviceId(deviceId) {
831
837
  const devicePeers = this.#peers.get(deviceId)
832
838
  if (!devicePeers || devicePeers.size === 0) {
833
- throw new UnknownPeerError('Unknown peer ' + deviceId.slice(0, 7))
839
+ throw new UnknownPeerError({ deviceId: deviceId.slice(0, 7) })
834
840
  }
835
841
  const peer = chooseDevicePeer(devicePeers)
836
842
  if (peer) return peer
837
843
  return new Promise((resolve, reject) => {
838
844
  const timeoutId = setTimeout(() => {
839
845
  this.off('peers', onPeers)
840
- reject(new UnknownPeerError('Unknown peer ' + deviceId.slice(0, 7)))
846
+ reject(new UnknownPeerError({ deviceId: deviceId.slice(0, 7) }))
841
847
  }, DEDUPE_TIMEOUT)
842
848
 
843
849
  const onPeers = () => {
@@ -854,38 +860,14 @@ export class LocalPeers extends TypedEmitter {
854
860
  }
855
861
  }
856
862
 
857
- export { TimeoutError }
858
-
859
- export class UnknownPeerError extends Error {
860
- /** @param {string} [message] */
861
- constructor(message = 'UnknownPeerError') {
862
- super(message)
863
- this.name = 'UnknownPeerError'
864
- }
865
- }
866
-
867
- export class PeerDisconnectedError extends Error {
868
- /** @param {string} [message] */
869
- constructor(message = 'Peer disconnected') {
870
- super(message)
871
- this.name = 'PeerDisconnectedError'
872
- }
873
- }
874
-
875
- export class PeerFailedConnectionError extends Error {
876
- /** @param {string} [message] */
877
- constructor(message = 'PeerFailedConnectionError') {
878
- super(message)
879
- this.name = 'PeerFailedConnectionError'
880
- }
881
- }
882
-
883
863
  /**
884
864
  * @param {Readonly<Uint8Array>} id
885
865
  * @throws if the invite ID is too short
886
866
  */
887
867
  function assertInviteIdIsValid(id) {
888
- assert(id.byteLength >= 32, 'Invite ID must be >= 32 bytes')
868
+ if (id.byteLength < 32) {
869
+ throw new InvalidInviteError('Invite ID must be >= 32 bytes')
870
+ }
889
871
  }
890
872
 
891
873
  /**
@@ -896,9 +878,15 @@ function assertInviteIdIsValid(id) {
896
878
  function parseInvite(data) {
897
879
  const result = Invite.decode(data)
898
880
  assertInviteIdIsValid(result.inviteId)
899
- assert(result.projectInviteId.length, 'Invite must have project invite ID')
900
- assert(!isBlank(result.projectName), 'Invite project name cannot be blank')
901
- assert(!isBlank(result.invitorName), 'Invite invitor name cannot be blank')
881
+ if (!result.projectInviteId.length) {
882
+ throw new InvalidInviteError('Invite must have project invite ID')
883
+ }
884
+ if (isBlank(result.projectName)) {
885
+ throw new InvalidInviteError('Invite project name cannot be blank')
886
+ }
887
+ if (isBlank(result.invitorName)) {
888
+ throw new InvalidInviteError('Invite invitor name cannot be blank')
889
+ }
902
890
  return result
903
891
  }
904
892
 
@@ -932,11 +920,16 @@ function parseInviteResponse(data) {
932
920
  function parseProjectJoinDetails(data) {
933
921
  const result = ProjectJoinDetails.decode(data)
934
922
  assertInviteIdIsValid(result.inviteId)
935
- assert(result.projectKey.length, 'Project join details must have project key')
936
- assert(
937
- result.encryptionKeys?.auth?.byteLength,
938
- 'Project join details must have auth encryption keys'
939
- )
923
+ if (!result.projectKey.length) {
924
+ throw new InvalidProjectJoinDetailsError(
925
+ 'Project join details must have project key'
926
+ )
927
+ }
928
+ if (!result.encryptionKeys?.auth?.byteLength) {
929
+ throw new InvalidProjectJoinDetailsError(
930
+ 'Project join details must have auth encryption keys'
931
+ )
932
+ }
940
933
  return result
941
934
  }
942
935
 
package/src/logger.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import createDebug from 'debug'
2
+ import ensureError from 'ensure-error'
2
3
  import { discoveryKey } from 'hypercore-crypto'
3
4
  import mapObject from 'map-obj'
4
5
  import util from 'util'
@@ -69,7 +70,7 @@ createDebug.formatters.X = function (v) {
69
70
  breakLength: 90,
70
71
  })
71
72
  } catch (e) {
72
- return `[ERROR: $(e.message)]`
73
+ return `[ERROR: ${ensureError(e).message}]`
73
74
  }
74
75
  }
75
76
 
@@ -6,7 +6,6 @@ import { eq, and } from 'drizzle-orm'
6
6
  import { drizzle } from 'drizzle-orm/better-sqlite3'
7
7
  import Hypercore from 'hypercore'
8
8
  import { TypedEmitter } from 'tiny-typed-emitter'
9
- import pTimeout from 'p-timeout'
10
9
  import { createRequire } from 'module'
11
10
 
12
11
  import { IndexWriter } from './index-writer/index.js'
@@ -36,6 +35,7 @@ import {
36
35
  projectKeyToId,
37
36
  projectKeyToProjectInviteId,
38
37
  projectKeyToPublicId,
38
+ timeoutPromise,
39
39
  } from './utils.js'
40
40
  import { openedNoiseSecretStream } from './lib/noise-secret-stream-helpers.js'
41
41
  import { omit } from './lib/omit.js'
@@ -49,7 +49,14 @@ import { InviteApi } from './invite/invite-api.js'
49
49
  import { LocalDiscovery } from './discovery/local-discovery.js'
50
50
  import { Logger } from './logger.js'
51
51
  import { kRequestFullStop, kRescindFullStopRequest } from './sync/sync-api.js'
52
- import { NotFoundError } from './errors.js'
52
+ import {
53
+ EncryptionKeysNotFoundError,
54
+ ensureKnownError,
55
+ ExhaustivenessError,
56
+ FailedToSetIsArchiveDeviceError,
57
+ NotFoundError,
58
+ ProjectExistsError,
59
+ } from './errors.js'
53
60
  import { WebSocket } from 'ws'
54
61
  import { excludeKeys } from 'filter-obj'
55
62
  import { migrate } from './lib/drizzle-helpers.js'
@@ -63,7 +70,7 @@ import { migrate } from './lib/drizzle-helpers.js'
63
70
  /** @import { ProjectSettings, ProjectSettingsValue } from '@comapeo/schema' */
64
71
 
65
72
  /** @typedef {SetNonNullable<ProjectKeys, 'encryptionKeys'>} ValidatedProjectKeys */
66
- /** @typedef {Pick<ProjectJoinDetails, 'projectKey' | 'encryptionKeys'> & { projectName: string, projectColor?: string, projectDescription?: string, sendStats?: boolean }} ProjectToAddDetails */
73
+ /** @typedef {Pick<ProjectJoinDetails, 'projectKey' | 'encryptionKeys'> & { projectName: string, projectColor?: string, projectDescription?: string, sendStats?: boolean, invitorWroteDeviceInfo? : boolean }} ProjectToAddDetails */
67
74
  /** @typedef {Pick<ProjectSettings, 'createdAt' | 'updatedAt' | 'name' | 'projectColor' | 'projectDescription' | 'sendStats'>} ListedProjectSettings */
68
75
  /** @typedef {ListedProjectSettings & { status: 'joined', projectId: string } | ProjectInfo & { status: 'joining' | 'left', projectId: string }} ListedProject */
69
76
 
@@ -277,7 +284,7 @@ export class MapeoManager extends TypedEmitter {
277
284
  break
278
285
  }
279
286
  default: {
280
- throw new Error(`Unsupported media type ${mediaType}`)
287
+ throw new ExhaustivenessError({ value: mediaType })
281
288
  }
282
289
  }
283
290
 
@@ -587,11 +594,11 @@ export class MapeoManager extends TypedEmitter {
587
594
 
588
595
  /**
589
596
  *
590
- * @param {Error} err
597
+ * @param {Error} e
591
598
  * @param {MapShareExtension} mapShareExtension
592
599
  */
593
- const onMapShareError = (err, mapShareExtension) => {
594
- this.emit('map-share-error', err, mapShareExtension)
600
+ const onMapShareError = (e, mapShareExtension) => {
601
+ this.emit('map-share-error', e, mapShareExtension)
595
602
  }
596
603
 
597
604
  project.once('close', () => {
@@ -696,6 +703,7 @@ export class MapeoManager extends TypedEmitter {
696
703
  projectColor,
697
704
  projectDescription,
698
705
  sendStats = false,
706
+ invitorWroteDeviceInfo = false,
699
707
  },
700
708
  { waitForSync = true } = {}
701
709
  ) => {
@@ -715,7 +723,7 @@ export class MapeoManager extends TypedEmitter {
715
723
  .get()
716
724
 
717
725
  if (existingProject && existingProject.hasLeftProject !== true) {
718
- throw new Error(`Project with ID ${projectPublicId} already exists`)
726
+ throw new ProjectExistsError({ projectPublicId })
719
727
  }
720
728
 
721
729
  // 2. Check for an active project
@@ -770,7 +778,7 @@ export class MapeoManager extends TypedEmitter {
770
778
  .delete(projectKeysTable)
771
779
  .where(eq(projectKeysTable.projectId, projectId))
772
780
  .run()
773
- throw e
781
+ throw ensureKnownError(e)
774
782
  }
775
783
 
776
784
  // Make sure to clean up when closed
@@ -778,18 +786,21 @@ export class MapeoManager extends TypedEmitter {
778
786
  this.#activeProjects.delete(projectPublicId)
779
787
  })
780
788
 
781
- try {
782
- const deviceInfo = this.getDeviceInfo()
783
- if (hasSavedDeviceInfo(deviceInfo)) {
784
- await project[kSetOwnDeviceInfo](deviceInfo)
789
+ // Only write info on invite if configured
790
+ if (!invitorWroteDeviceInfo) {
791
+ try {
792
+ const deviceInfo = this.getDeviceInfo()
793
+ if (hasSavedDeviceInfo(deviceInfo)) {
794
+ await project[kSetOwnDeviceInfo](deviceInfo)
795
+ }
796
+ } catch (e) {
797
+ // Can ignore an error trying to write device info
798
+ this.#l.log(
799
+ 'ERROR: failed to write project %h deviceInfo %o',
800
+ projectKey,
801
+ e
802
+ )
785
803
  }
786
- } catch (e) {
787
- // Can ignore an error trying to write device info
788
- this.#l.log(
789
- 'ERROR: failed to write project %h deviceInfo %o',
790
- projectKey,
791
- e
792
- )
793
804
  }
794
805
 
795
806
  // 5. Wait for initial project sync
@@ -899,7 +910,7 @@ export class MapeoManager extends TypedEmitter {
899
910
  })
900
911
  .run()
901
912
  if (!result || result.changes === 0) {
902
- throw new Error('Failed to set isArchiveDevice')
913
+ throw new FailedToSetIsArchiveDeviceError()
903
914
  }
904
915
  for (const project of this.#activeProjects.values()) {
905
916
  project[kSetIsArchiveDevice](isArchiveDevice)
@@ -1036,7 +1047,9 @@ export class MapeoManager extends TypedEmitter {
1036
1047
  }
1037
1048
 
1038
1049
  async getMapStyleJsonUrl() {
1039
- await pTimeout(this.#fastify.ready(), { milliseconds: 1000 })
1050
+ await timeoutPromise(Promise.resolve(this.#fastify.ready()), {
1051
+ milliseconds: 1000,
1052
+ })
1040
1053
  return (await this.#getMediaBaseUrl('maps')) + '/style.json'
1041
1054
  }
1042
1055
 
@@ -1078,7 +1091,7 @@ function omitPeerProtomux(peers) {
1078
1091
  */
1079
1092
  function validateProjectKeys(projectKeys) {
1080
1093
  if (!projectKeys.encryptionKeys) {
1081
- throw new Error('encryptionKeys should not be undefined')
1094
+ throw new EncryptionKeysNotFoundError()
1082
1095
  }
1083
1096
  }
1084
1097