@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/member-api.js
CHANGED
|
@@ -5,9 +5,7 @@ import { TypedEmitter } from 'tiny-typed-emitter'
|
|
|
5
5
|
import { pEvent } from 'p-event'
|
|
6
6
|
import { InviteResponse_Decision } from './generated/rpc.js'
|
|
7
7
|
import {
|
|
8
|
-
assert,
|
|
9
8
|
noop,
|
|
10
|
-
ExhaustivenessError,
|
|
11
9
|
projectKeyToId,
|
|
12
10
|
projectKeyToProjectInviteId,
|
|
13
11
|
projectKeyToPublicId,
|
|
@@ -17,26 +15,51 @@ import { keyBy } from './lib/key-by.js'
|
|
|
17
15
|
import { abortSignalAny } from './lib/ponyfills.js'
|
|
18
16
|
import timingSafeEqual from 'string-timing-safe-equal'
|
|
19
17
|
import { isHostnameIpAddress } from './lib/is-hostname-ip-address.js'
|
|
20
|
-
import {
|
|
21
|
-
|
|
18
|
+
import {
|
|
19
|
+
AlreadyBlockedError,
|
|
20
|
+
DeviceIdNotForServerError,
|
|
21
|
+
ensureKnownError,
|
|
22
|
+
InvalidServerResponseError,
|
|
23
|
+
InvalidUrlError,
|
|
24
|
+
InviteAbortedError,
|
|
25
|
+
IncompleteProjectDataError,
|
|
26
|
+
MissingOwnDeviceInfoError,
|
|
27
|
+
NetworkError,
|
|
28
|
+
ProjectDetailsSendFailError,
|
|
29
|
+
ProjectNotInAllowlistError,
|
|
30
|
+
ServerTooManyProjectsError,
|
|
31
|
+
ExhaustivenessError,
|
|
32
|
+
InvalidRoleIDForNewInviteError,
|
|
33
|
+
InvalidProjectNameError,
|
|
34
|
+
UnexpectedError,
|
|
35
|
+
AlreadyInvitingError,
|
|
36
|
+
InvalidResponseBodyError,
|
|
37
|
+
RPCDisconnectBeforeAckError,
|
|
38
|
+
} from './errors.js'
|
|
22
39
|
import { wsCoreReplicator } from './lib/ws-core-replicator.js'
|
|
23
40
|
import {
|
|
24
41
|
BLOCKED_ROLE_ID,
|
|
42
|
+
COORDINATOR_ROLE_ID,
|
|
43
|
+
CREATOR_ROLE_ID,
|
|
25
44
|
LEFT_ROLE_ID,
|
|
26
45
|
MEMBER_ROLE_ID,
|
|
27
46
|
ROLES,
|
|
28
47
|
isRoleIdForNewInvite,
|
|
29
48
|
} from './roles.js'
|
|
49
|
+
import { kCreateOrUpdateWithDocId } from './datatype/index.js'
|
|
50
|
+
|
|
51
|
+
const ACTIVE_ROLE_IDS = [CREATOR_ROLE_ID, MEMBER_ROLE_ID, COORDINATOR_ROLE_ID]
|
|
52
|
+
|
|
30
53
|
/**
|
|
31
54
|
* @import {
|
|
32
55
|
* DeviceInfo,
|
|
33
56
|
* DeviceInfoValue,
|
|
34
57
|
* ProjectSettings,
|
|
35
|
-
* ProjectSettingsValue
|
|
58
|
+
* ProjectSettingsValue,
|
|
36
59
|
* } from '@comapeo/schema'
|
|
37
60
|
*/
|
|
38
61
|
/** @import { Promisable } from 'type-fest' */
|
|
39
|
-
/** @import { Invite, InviteResponse } from './generated/rpc.js' */
|
|
62
|
+
/** @import { DeviceInfo_DeviceType, Invite, InviteResponse } from './generated/rpc.js' */
|
|
40
63
|
/** @import { DataType } from './datatype/index.js' */
|
|
41
64
|
/** @import { DataStore } from './datastore/index.js' */
|
|
42
65
|
/** @import { deviceInfoTable } from './schema/project.js' */
|
|
@@ -56,6 +79,16 @@ import {
|
|
|
56
79
|
* @prop {string} selfHostedServerDetails.baseUrl
|
|
57
80
|
*/
|
|
58
81
|
|
|
82
|
+
/**
|
|
83
|
+
* @typedef {object} InvitePeerInfo
|
|
84
|
+
* @prop {DeviceInfo['name']} name
|
|
85
|
+
* @prop {DeviceInfo['deviceType']} deviceType
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @typedef {Omit<MemberInfo, 'role'> & {role: import('./roles.js').Role<typeof MEMBER_ROLE_ID | typeof COORDINATOR_ROLE_ID | typeof CREATOR_ROLE_ID>}} ActiveMemberInfo
|
|
90
|
+
*/
|
|
91
|
+
|
|
59
92
|
export class MemberApi extends TypedEmitter {
|
|
60
93
|
#ownDeviceId
|
|
61
94
|
#roles
|
|
@@ -86,7 +119,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
86
119
|
* @param {() => ReplicationStream} opts.getReplicationStream
|
|
87
120
|
* @param {(deviceId: string, abortSignal: AbortSignal) => Promise<void>} opts.waitForInitialSyncWithPeer
|
|
88
121
|
* @param {Object} opts.dataTypes
|
|
89
|
-
* @param {Pick<DeviceInfoDataType, 'getByDocId' | 'getMany'>} opts.dataTypes.deviceInfo
|
|
122
|
+
* @param {Pick<DeviceInfoDataType, 'getByDocId' | 'getMany' | kCreateOrUpdateWithDocId>} opts.dataTypes.deviceInfo
|
|
90
123
|
* @param {Pick<ProjectDataType, 'getByDocId'>} opts.dataTypes.project
|
|
91
124
|
* @param {Logger} [opts.logger]
|
|
92
125
|
*/
|
|
@@ -130,6 +163,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
130
163
|
* @param {string} [opts.roleDescription]
|
|
131
164
|
* @param {Buffer} [opts.__testOnlyInviteId] Hard-code the invite ID. Only for tests.
|
|
132
165
|
* @param {number} [opts.initialSyncTimeoutMs=5000]
|
|
166
|
+
* @param {InvitePeerInfo} [opts.peerInfo]
|
|
133
167
|
* @returns {Promise<(
|
|
134
168
|
* typeof InviteResponse_Decision.ACCEPT |
|
|
135
169
|
* typeof InviteResponse_Decision.REJECT |
|
|
@@ -144,13 +178,15 @@ export class MemberApi extends TypedEmitter {
|
|
|
144
178
|
roleDescription,
|
|
145
179
|
__testOnlyInviteId,
|
|
146
180
|
initialSyncTimeoutMs = 5000,
|
|
181
|
+
peerInfo,
|
|
147
182
|
}
|
|
148
183
|
) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
184
|
+
if (!isRoleIdForNewInvite(roleId)) {
|
|
185
|
+
throw new InvalidRoleIDForNewInviteError({ roleId })
|
|
186
|
+
}
|
|
187
|
+
if (this.#outboundInvitesByDevice.has(deviceId)) {
|
|
188
|
+
throw new AlreadyInvitingError()
|
|
189
|
+
}
|
|
154
190
|
|
|
155
191
|
const abortController = new AbortController()
|
|
156
192
|
const abortSignal = abortController.signal
|
|
@@ -160,10 +196,11 @@ export class MemberApi extends TypedEmitter {
|
|
|
160
196
|
const { name: invitorName } = await this.getById(this.#ownDeviceId)
|
|
161
197
|
// since we are always getting #ownDeviceId,
|
|
162
198
|
// this should never throw (see comment on getById), but it pleases ts
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
199
|
+
if (!invitorName) {
|
|
200
|
+
throw new UnexpectedError(
|
|
201
|
+
'Internal error trying to read own device name for this invite'
|
|
202
|
+
)
|
|
203
|
+
}
|
|
167
204
|
|
|
168
205
|
abortSignal.throwIfAborted()
|
|
169
206
|
|
|
@@ -172,11 +209,14 @@ export class MemberApi extends TypedEmitter {
|
|
|
172
209
|
const projectInviteId = projectKeyToProjectInviteId(this.#projectKey)
|
|
173
210
|
const project = await this.#dataTypes.project.getByDocId(projectId)
|
|
174
211
|
const projectName = project.name
|
|
175
|
-
|
|
212
|
+
if (!projectName) {
|
|
213
|
+
throw new InvalidProjectNameError()
|
|
214
|
+
}
|
|
176
215
|
|
|
177
216
|
const projectColor = project.projectColor
|
|
178
217
|
const projectDescription = project.projectDescription
|
|
179
218
|
const sendStats = project.sendStats
|
|
219
|
+
const invitorWroteDeviceInfo = !!peerInfo
|
|
180
220
|
|
|
181
221
|
abortSignal.throwIfAborted()
|
|
182
222
|
|
|
@@ -190,6 +230,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
190
230
|
roleDescription,
|
|
191
231
|
invitorName,
|
|
192
232
|
sendStats,
|
|
233
|
+
invitorWroteDeviceInfo,
|
|
193
234
|
}
|
|
194
235
|
|
|
195
236
|
const inviteResponse = await this.#sendInviteAndGetResponse(
|
|
@@ -221,6 +262,20 @@ export class MemberApi extends TypedEmitter {
|
|
|
221
262
|
}
|
|
222
263
|
await this.#roles.assignRole(deviceId, roleId)
|
|
223
264
|
|
|
265
|
+
if (invitorWroteDeviceInfo) {
|
|
266
|
+
const { name, deviceType } = peerInfo
|
|
267
|
+
const doc = {
|
|
268
|
+
name: name,
|
|
269
|
+
deviceType: deviceType,
|
|
270
|
+
selfHostedServerDetails: undefined,
|
|
271
|
+
schemaName: /** @type {const} */ ('deviceInfo'),
|
|
272
|
+
}
|
|
273
|
+
await this.#dataTypes.deviceInfo[kCreateOrUpdateWithDocId](
|
|
274
|
+
deviceId,
|
|
275
|
+
doc
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
224
279
|
try {
|
|
225
280
|
let abortSync = new AbortController().signal
|
|
226
281
|
if (initialSyncTimeoutMs) {
|
|
@@ -234,14 +289,14 @@ export class MemberApi extends TypedEmitter {
|
|
|
234
289
|
|
|
235
290
|
return inviteResponse.decision
|
|
236
291
|
default:
|
|
237
|
-
throw new ExhaustivenessError(inviteResponse.decision)
|
|
292
|
+
throw new ExhaustivenessError({ value: inviteResponse.decision })
|
|
238
293
|
}
|
|
239
|
-
} catch (
|
|
240
|
-
if (
|
|
241
|
-
this.#l.log('ERROR: Disconnect before ack',
|
|
294
|
+
} catch (e) {
|
|
295
|
+
if (e instanceof RPCDisconnectBeforeAckError) {
|
|
296
|
+
this.#l.log('ERROR: Disconnect before ack', e)
|
|
242
297
|
throw new InviteAbortedError()
|
|
243
298
|
}
|
|
244
|
-
throw
|
|
299
|
+
throw ensureKnownError(e)
|
|
245
300
|
} finally {
|
|
246
301
|
this.#outboundInvitesByDevice.delete(deviceId)
|
|
247
302
|
}
|
|
@@ -285,13 +340,13 @@ export class MemberApi extends TypedEmitter {
|
|
|
285
340
|
try {
|
|
286
341
|
await this.#rpc.sendInvite(deviceId, invite)
|
|
287
342
|
return await responsePromise
|
|
288
|
-
} catch (
|
|
289
|
-
if (
|
|
290
|
-
this.#l.log('ERROR: Timed out sending invite',
|
|
343
|
+
} catch (e) {
|
|
344
|
+
if (e instanceof Error && e.name === 'AbortError') {
|
|
345
|
+
this.#l.log('ERROR: Timed out sending invite', e)
|
|
291
346
|
throw new InviteAbortedError()
|
|
292
347
|
} else {
|
|
293
|
-
this.#l.log('ERROR: Unexpected error during invite send',
|
|
294
|
-
throw
|
|
348
|
+
this.#l.log('ERROR: Unexpected error during invite send', e)
|
|
349
|
+
throw ensureKnownError(e)
|
|
295
350
|
}
|
|
296
351
|
} finally {
|
|
297
352
|
abortController.abort()
|
|
@@ -342,7 +397,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
342
397
|
if (
|
|
343
398
|
!isValidServerBaseUrl(baseUrl, { dangerouslyAllowInsecureConnections })
|
|
344
399
|
) {
|
|
345
|
-
throw new
|
|
400
|
+
throw new InvalidUrlError()
|
|
346
401
|
}
|
|
347
402
|
|
|
348
403
|
const { serverDeviceId } = await this.#addServerToProject(baseUrl)
|
|
@@ -367,7 +422,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
367
422
|
const { roleId } = member.role
|
|
368
423
|
|
|
369
424
|
if (roleId === BLOCKED_ROLE_ID || roleId === LEFT_ROLE_ID) {
|
|
370
|
-
throw new
|
|
425
|
+
throw new AlreadyBlockedError()
|
|
371
426
|
}
|
|
372
427
|
|
|
373
428
|
// Add blocked role to project
|
|
@@ -392,14 +447,12 @@ export class MemberApi extends TypedEmitter {
|
|
|
392
447
|
const member = await this.getById(serverDeviceId)
|
|
393
448
|
|
|
394
449
|
if (!member.selfHostedServerDetails) {
|
|
395
|
-
throw new
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
)
|
|
450
|
+
throw new DeviceIdNotForServerError({
|
|
451
|
+
deviceId: serverDeviceId.slice(0, 7),
|
|
452
|
+
})
|
|
399
453
|
}
|
|
400
|
-
|
|
401
454
|
if (member.role.roleId === BLOCKED_ROLE_ID) {
|
|
402
|
-
throw new
|
|
455
|
+
throw new AlreadyBlockedError()
|
|
403
456
|
}
|
|
404
457
|
|
|
405
458
|
const { baseUrl } = member.selfHostedServerDetails
|
|
@@ -422,10 +475,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
422
475
|
async #addServerToProject(baseUrl) {
|
|
423
476
|
const projectName = await this.#getProjectName()
|
|
424
477
|
if (!projectName) {
|
|
425
|
-
throw new
|
|
426
|
-
'MISSING_DATA',
|
|
427
|
-
'Project must have name to add server peer'
|
|
428
|
-
)
|
|
478
|
+
throw new IncompleteProjectDataError()
|
|
429
479
|
}
|
|
430
480
|
|
|
431
481
|
const requestUrl = new URL('projects', baseUrl)
|
|
@@ -448,13 +498,10 @@ export class MemberApi extends TypedEmitter {
|
|
|
448
498
|
body: JSON.stringify(requestBody),
|
|
449
499
|
headers: { 'Content-Type': 'application/json' },
|
|
450
500
|
})
|
|
451
|
-
} catch (
|
|
452
|
-
throw new
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
err
|
|
456
|
-
)}`
|
|
457
|
-
)
|
|
501
|
+
} catch (e) {
|
|
502
|
+
throw new NetworkError('Failed to add server peer due to network error', {
|
|
503
|
+
cause: e,
|
|
504
|
+
})
|
|
458
505
|
}
|
|
459
506
|
|
|
460
507
|
return await parseAddServerResponse(response)
|
|
@@ -483,18 +530,12 @@ export class MemberApi extends TypedEmitter {
|
|
|
483
530
|
|
|
484
531
|
try {
|
|
485
532
|
await pEvent(websocket, 'open', { rejectionEvents: ['error'] })
|
|
486
|
-
} catch (
|
|
487
|
-
throw new
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
'Failed to open the socket',
|
|
493
|
-
rejectionEvent &&
|
|
494
|
-
typeof rejectionEvent === 'object' &&
|
|
495
|
-
'error' in rejectionEvent
|
|
496
|
-
? { cause: rejectionEvent.error }
|
|
497
|
-
: { cause: rejectionEvent }
|
|
533
|
+
} catch (e) {
|
|
534
|
+
throw new InvalidServerResponseError(
|
|
535
|
+
'Failed to open websocket for initial sync',
|
|
536
|
+
e && typeof e === 'object' && 'error' in e
|
|
537
|
+
? { cause: e.error }
|
|
538
|
+
: { cause: e }
|
|
498
539
|
)
|
|
499
540
|
}
|
|
500
541
|
|
|
@@ -538,32 +579,54 @@ export class MemberApi extends TypedEmitter {
|
|
|
538
579
|
const result = { deviceId, role }
|
|
539
580
|
|
|
540
581
|
try {
|
|
541
|
-
const
|
|
542
|
-
deviceId,
|
|
543
|
-
'config'
|
|
544
|
-
)
|
|
545
|
-
|
|
546
|
-
const deviceInfo = await this.#dataTypes.deviceInfo.getByDocId(
|
|
547
|
-
configCoreId
|
|
548
|
-
)
|
|
582
|
+
const deviceInfo = await this.#getDeviceInfo(deviceId)
|
|
549
583
|
|
|
550
584
|
result.name = deviceInfo.name
|
|
551
585
|
result.deviceType = deviceInfo.deviceType
|
|
552
586
|
result.joinedAt = deviceInfo.createdAt
|
|
553
587
|
result.selfHostedServerDetails = deviceInfo.selfHostedServerDetails
|
|
554
|
-
} catch (
|
|
588
|
+
} catch (e) {
|
|
555
589
|
// Attempting to get someone else may throw because sync hasn't occurred or completed
|
|
556
590
|
// Only throw if attempting to get themself since the relevant information should be available
|
|
557
|
-
if (deviceId === this.#ownDeviceId)
|
|
591
|
+
if (deviceId === this.#ownDeviceId) {
|
|
592
|
+
throw new MissingOwnDeviceInfoError({ cause: e })
|
|
593
|
+
}
|
|
558
594
|
}
|
|
559
595
|
|
|
560
596
|
return result
|
|
561
597
|
}
|
|
562
598
|
|
|
563
599
|
/**
|
|
564
|
-
* @
|
|
600
|
+
* @param {string} deviceId
|
|
601
|
+
* @returns {Promise<DeviceInfo>}
|
|
602
|
+
*/
|
|
603
|
+
async #getDeviceInfo(deviceId) {
|
|
604
|
+
try {
|
|
605
|
+
return await this.#dataTypes.deviceInfo.getByDocId(deviceId)
|
|
606
|
+
} catch (e) {
|
|
607
|
+
const configCoreId = await this.#coreOwnership.getCoreId(
|
|
608
|
+
deviceId,
|
|
609
|
+
'config'
|
|
610
|
+
)
|
|
611
|
+
return this.#dataTypes.deviceInfo.getByDocId(configCoreId)
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* @overload
|
|
617
|
+
* @returns {Promise<Array<ActiveMemberInfo>>}
|
|
565
618
|
*/
|
|
566
|
-
|
|
619
|
+
/**
|
|
620
|
+
* @template {boolean} [T=false]
|
|
621
|
+
*
|
|
622
|
+
* @overload
|
|
623
|
+
* @param {Object} opts
|
|
624
|
+
* @param {T} [opts.includeLeft=false]
|
|
625
|
+
*
|
|
626
|
+
* @returns {Promise<T extends true ? Array<MemberInfo> : Array<ActiveMemberInfo>>}
|
|
627
|
+
*
|
|
628
|
+
*/
|
|
629
|
+
async getMany({ includeLeft = false } = {}) {
|
|
567
630
|
const [allRoles, allDeviceInfo] = await Promise.all([
|
|
568
631
|
this.#roles.getAll(),
|
|
569
632
|
this.#dataTypes.deviceInfo.getMany(),
|
|
@@ -571,8 +634,17 @@ export class MemberApi extends TypedEmitter {
|
|
|
571
634
|
|
|
572
635
|
const deviceInfoByConfigCoreId = keyBy(allDeviceInfo, ({ docId }) => docId)
|
|
573
636
|
|
|
574
|
-
|
|
575
|
-
|
|
637
|
+
/**
|
|
638
|
+
* @type {Array<Promise<MemberInfo>>}
|
|
639
|
+
*/
|
|
640
|
+
const activeMemberInfoPromises = []
|
|
641
|
+
|
|
642
|
+
for (const [deviceId, role] of allRoles.entries()) {
|
|
643
|
+
if (!includeLeft && !isActiveMemberRole(role)) {
|
|
644
|
+
continue
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const getMemberInfo = async () => {
|
|
576
648
|
/** @type {MemberInfo} */
|
|
577
649
|
const memberInfo = { deviceId, role }
|
|
578
650
|
|
|
@@ -589,15 +661,20 @@ export class MemberApi extends TypedEmitter {
|
|
|
589
661
|
memberInfo.joinedAt = deviceInfo?.createdAt
|
|
590
662
|
memberInfo.selfHostedServerDetails =
|
|
591
663
|
deviceInfo?.selfHostedServerDetails
|
|
592
|
-
} catch (
|
|
664
|
+
} catch (e) {
|
|
593
665
|
// Attempting to get someone else may throw because sync hasn't occurred or completed
|
|
594
666
|
// Only throw if attempting to get themself since the relevant information should be available
|
|
595
|
-
if (deviceId === this.#ownDeviceId)
|
|
667
|
+
if (deviceId === this.#ownDeviceId) {
|
|
668
|
+
throw new MissingOwnDeviceInfoError({ cause: e })
|
|
669
|
+
}
|
|
596
670
|
}
|
|
597
671
|
|
|
598
672
|
return memberInfo
|
|
599
|
-
}
|
|
600
|
-
|
|
673
|
+
}
|
|
674
|
+
activeMemberInfoPromises.push(getMemberInfo())
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return Promise.all(activeMemberInfoPromises)
|
|
601
678
|
}
|
|
602
679
|
|
|
603
680
|
/**
|
|
@@ -610,6 +687,15 @@ export class MemberApi extends TypedEmitter {
|
|
|
610
687
|
}
|
|
611
688
|
}
|
|
612
689
|
|
|
690
|
+
/**
|
|
691
|
+
* @param {import('./roles.js').Role} role
|
|
692
|
+
*
|
|
693
|
+
* @returns {role is ActiveMemberInfo['role']}
|
|
694
|
+
*/
|
|
695
|
+
function isActiveMemberRole(role) {
|
|
696
|
+
return ACTIVE_ROLE_IDS.includes(role.roleId)
|
|
697
|
+
}
|
|
698
|
+
|
|
613
699
|
/**
|
|
614
700
|
* @param {string} baseUrl
|
|
615
701
|
* @param {object} options
|
|
@@ -625,7 +711,7 @@ function isValidServerBaseUrl(
|
|
|
625
711
|
/** @type {URL} */ let url
|
|
626
712
|
try {
|
|
627
713
|
url = new URL(baseUrl)
|
|
628
|
-
} catch
|
|
714
|
+
} catch {
|
|
629
715
|
return false
|
|
630
716
|
}
|
|
631
717
|
|
|
@@ -670,20 +756,22 @@ async function parseAddServerResponse(response) {
|
|
|
670
756
|
if (response.status === 200) {
|
|
671
757
|
try {
|
|
672
758
|
const responseBody = await response.json()
|
|
673
|
-
|
|
674
|
-
|
|
759
|
+
if (
|
|
760
|
+
!(
|
|
761
|
+
responseBody &&
|
|
675
762
|
typeof responseBody === 'object' &&
|
|
676
763
|
'data' in responseBody &&
|
|
677
764
|
responseBody.data &&
|
|
678
765
|
typeof responseBody.data === 'object' &&
|
|
679
766
|
'deviceId' in responseBody.data &&
|
|
680
|
-
typeof responseBody.data.deviceId === 'string'
|
|
681
|
-
|
|
682
|
-
)
|
|
767
|
+
typeof responseBody.data.deviceId === 'string'
|
|
768
|
+
)
|
|
769
|
+
) {
|
|
770
|
+
throw new InvalidResponseBodyError()
|
|
771
|
+
}
|
|
683
772
|
return { serverDeviceId: responseBody.data.deviceId }
|
|
684
|
-
} catch
|
|
685
|
-
throw new
|
|
686
|
-
'INVALID_SERVER_RESPONSE',
|
|
773
|
+
} catch {
|
|
774
|
+
throw new InvalidServerResponseError(
|
|
687
775
|
"Failed to add server peer because we couldn't parse the response"
|
|
688
776
|
)
|
|
689
777
|
}
|
|
@@ -692,7 +780,7 @@ async function parseAddServerResponse(response) {
|
|
|
692
780
|
let responseBody
|
|
693
781
|
try {
|
|
694
782
|
responseBody = await response.json()
|
|
695
|
-
} catch
|
|
783
|
+
} catch {
|
|
696
784
|
responseBody = null
|
|
697
785
|
}
|
|
698
786
|
if (
|
|
@@ -705,22 +793,15 @@ async function parseAddServerResponse(response) {
|
|
|
705
793
|
) {
|
|
706
794
|
switch (responseBody.error.code) {
|
|
707
795
|
case 'PROJECT_NOT_IN_ALLOWLIST':
|
|
708
|
-
throw new
|
|
709
|
-
'PROJECT_NOT_IN_SERVER_ALLOWLIST',
|
|
710
|
-
"The server only allows specific projects to be added, and this isn't one of them"
|
|
711
|
-
)
|
|
796
|
+
throw new ProjectNotInAllowlistError()
|
|
712
797
|
case 'TOO_MANY_PROJECTS':
|
|
713
|
-
throw new
|
|
714
|
-
'SERVER_HAS_TOO_MANY_PROJECTS',
|
|
715
|
-
"The server limits the number of projects it can have and it's at the limit"
|
|
716
|
-
)
|
|
798
|
+
throw new ServerTooManyProjectsError()
|
|
717
799
|
default:
|
|
718
800
|
break
|
|
719
801
|
}
|
|
720
802
|
}
|
|
721
803
|
|
|
722
|
-
throw new
|
|
723
|
-
'INVALID_SERVER_RESPONSE',
|
|
804
|
+
throw new InvalidServerResponseError(
|
|
724
805
|
`Failed to add server peer due to HTTP status code ${response.status}`
|
|
725
806
|
)
|
|
726
807
|
}
|
package/src/roles.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { currentSchemaVersions } from '@comapeo/schema'
|
|
2
2
|
import mapObject from 'map-obj'
|
|
3
3
|
import { kCreateWithDocId, kDataStore } from './datatype/index.js'
|
|
4
|
-
import {
|
|
5
|
-
import { nullIfNotFound } from './errors.js'
|
|
4
|
+
import { setHas } from './utils.js'
|
|
5
|
+
import { nullIfNotFound, RoleAssignError } from './errors.js'
|
|
6
6
|
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
7
7
|
/** @import { Namespace } from './types.js' */
|
|
8
8
|
|
|
@@ -332,7 +332,7 @@ export class Roles extends TypedEmitter {
|
|
|
332
332
|
// Default to creator role, but can be overwritten if a different role is
|
|
333
333
|
// set below
|
|
334
334
|
result.set(projectCreatorDeviceId, CREATOR_ROLE)
|
|
335
|
-
} catch
|
|
335
|
+
} catch {
|
|
336
336
|
// Not found, we don't know who the project creator is so we can't include
|
|
337
337
|
// them in the returned map
|
|
338
338
|
}
|
|
@@ -369,10 +369,11 @@ export class Roles extends TypedEmitter {
|
|
|
369
369
|
* @param {string} opts.reason
|
|
370
370
|
*/
|
|
371
371
|
async assignRole(deviceId, roleId, opts) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
372
|
+
if (!isRoleIdAssignableToAnyone(roleId)) {
|
|
373
|
+
throw new RoleAssignError(
|
|
374
|
+
`Role ID should be assignable to anyone but got ${roleId}`
|
|
375
|
+
)
|
|
376
|
+
}
|
|
376
377
|
|
|
377
378
|
let fromIndex = 0
|
|
378
379
|
let authCoreId
|
|
@@ -391,12 +392,12 @@ export class Roles extends TypedEmitter {
|
|
|
391
392
|
}
|
|
392
393
|
if (roleId === LEFT_ROLE_ID) {
|
|
393
394
|
if (deviceId !== this.#ownDeviceId) {
|
|
394
|
-
throw new
|
|
395
|
+
throw new RoleAssignError('Cannot assign LEFT role to another device')
|
|
395
396
|
}
|
|
396
397
|
} else {
|
|
397
398
|
const ownRole = await this.getRole(this.#ownDeviceId)
|
|
398
399
|
if (!ownRole.roleAssignment.includes(roleId)) {
|
|
399
|
-
throw new
|
|
400
|
+
throw new RoleAssignError('Lacks permission to assign role ' + roleId)
|
|
400
401
|
}
|
|
401
402
|
}
|
|
402
403
|
|
|
@@ -423,7 +424,7 @@ export class Roles extends TypedEmitter {
|
|
|
423
424
|
isAssigningProjectCreatorRole &&
|
|
424
425
|
roleId !== BLOCKED_ROLE_ID
|
|
425
426
|
) {
|
|
426
|
-
throw new
|
|
427
|
+
throw new RoleAssignError(
|
|
427
428
|
'Project creators can only be assigned the blocked role'
|
|
428
429
|
)
|
|
429
430
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { text, integer, real, sqliteTable } from 'drizzle-orm/sqlite-core'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
InvalidComapeoSchemaFormatError,
|
|
4
|
+
ExhaustivenessError,
|
|
5
|
+
} from '../errors.js'
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* @template {{ [ K in keyof TSchema['properties'] ]?: any }} TObjectType
|
|
@@ -20,14 +23,20 @@ export function jsonSchemaToDrizzleSqliteTable(
|
|
|
20
23
|
{ additionalColumns, primaryKey } = {}
|
|
21
24
|
) {
|
|
22
25
|
if (schema.type !== 'object' || !schema.properties) {
|
|
23
|
-
throw new
|
|
26
|
+
throw new InvalidComapeoSchemaFormatError({
|
|
27
|
+
tableName,
|
|
28
|
+
reason: 'Missing schema properties',
|
|
29
|
+
})
|
|
24
30
|
}
|
|
25
31
|
/** @type {Record<string, any>} */
|
|
26
32
|
const columns = {}
|
|
27
33
|
for (const [key, value] of Object.entries(schema.properties)) {
|
|
28
34
|
if (typeof value !== 'object') continue
|
|
29
35
|
if (isArray(value.type) || typeof value.type === 'undefined') {
|
|
30
|
-
throw new
|
|
36
|
+
throw new InvalidComapeoSchemaFormatError({
|
|
37
|
+
tableName,
|
|
38
|
+
reason: 'Columns must have single known type',
|
|
39
|
+
})
|
|
31
40
|
}
|
|
32
41
|
switch (value.type) {
|
|
33
42
|
case 'boolean':
|
|
@@ -56,7 +65,7 @@ export function jsonSchemaToDrizzleSqliteTable(
|
|
|
56
65
|
// Skip handling this right now
|
|
57
66
|
continue
|
|
58
67
|
default:
|
|
59
|
-
throw new ExhaustivenessError(value.type)
|
|
68
|
+
throw new ExhaustivenessError({ value: value.type })
|
|
60
69
|
}
|
|
61
70
|
if (isRequired(schema, key)) {
|
|
62
71
|
columns[key] = columns[key].notNull()
|
package/src/schema.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@comapeo/schema'
|
|
@@ -3,6 +3,7 @@ import RemoteBitfield, {
|
|
|
3
3
|
BITS_PER_PAGE,
|
|
4
4
|
} from '../core-manager/remote-bitfield.js'
|
|
5
5
|
import { Logger } from '../logger.js'
|
|
6
|
+
import { InvalidBitfieldIndexError } from '../errors.js'
|
|
6
7
|
/** @import { HypercorePeer, HypercoreRemoteBitfield, Namespace } from '../types.js' */
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -501,7 +502,7 @@ export function bitCount32(n) {
|
|
|
501
502
|
* @param {number} index
|
|
502
503
|
*/
|
|
503
504
|
function getBitfieldWord(bitfield, index) {
|
|
504
|
-
if (index % 32 !== 0) throw new
|
|
505
|
+
if (index % 32 !== 0) throw new InvalidBitfieldIndexError()
|
|
505
506
|
const j = index & (BITS_PER_PAGE - 1)
|
|
506
507
|
const i = (index - j) / BITS_PER_PAGE
|
|
507
508
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import mapObject from 'map-obj'
|
|
2
2
|
import { NAMESPACES, PRESYNC_NAMESPACES } from '../constants.js'
|
|
3
3
|
import { Logger } from '../logger.js'
|
|
4
|
-
import {
|
|
4
|
+
import { createMap } from '../utils.js'
|
|
5
5
|
import { unreplicate } from '../lib/hypercore-helpers.js'
|
|
6
|
+
import { ExhaustivenessError } from '../errors.js'
|
|
6
7
|
/** @import { CoreRecord } from '../core-manager/index.js' */
|
|
7
8
|
/** @import { Role } from '../roles.js' */
|
|
8
9
|
/** @import { SyncEnabledState } from './sync-api.js' */
|
|
@@ -195,7 +196,7 @@ export class PeerSyncController {
|
|
|
195
196
|
isAnySyncEnabled = isDataSyncEnabled = true
|
|
196
197
|
break
|
|
197
198
|
default:
|
|
198
|
-
throw new ExhaustivenessError(this.#syncEnabledState)
|
|
199
|
+
throw new ExhaustivenessError({ value: this.#syncEnabledState })
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
for (const ns of NAMESPACES) {
|
|
@@ -228,7 +229,7 @@ export class PeerSyncController {
|
|
|
228
229
|
this.#disableNamespace(ns)
|
|
229
230
|
}
|
|
230
231
|
} else {
|
|
231
|
-
throw new ExhaustivenessError(cap)
|
|
232
|
+
throw new ExhaustivenessError({ value: cap })
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
235
|
}
|