@comapeo/core 3.0.0 → 3.1.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/generated/rpc.d.ts +47 -0
- package/dist/generated/rpc.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/invite/invite-api.d.ts +4 -5
- package/dist/invite/invite-api.d.ts.map +1 -1
- package/dist/local-peers.d.ts +31 -0
- package/dist/local-peers.d.ts.map +1 -1
- package/dist/mapeo-manager.d.ts +30 -22
- package/dist/mapeo-manager.d.ts.map +1 -1
- package/dist/mapeo-project.d.ts +39 -1
- package/dist/mapeo-project.d.ts.map +1 -1
- package/dist/member-api.d.ts +15 -1
- package/dist/member-api.d.ts.map +1 -1
- package/dist/schema/client.d.ts +26 -3
- package/dist/schema/client.d.ts.map +1 -1
- package/dist/sync/sync-api.d.ts +4 -1
- package/dist/sync/sync-api.d.ts.map +1 -1
- package/drizzle/client/0002_brief_demogoblin.sql +2 -0
- package/drizzle/client/meta/0002_snapshot.json +220 -0
- package/drizzle/client/meta/_journal.json +7 -0
- package/package.json +3 -3
- package/src/generated/rpc.d.ts +47 -0
- package/src/generated/rpc.js +241 -3
- package/src/generated/rpc.ts +280 -1
- package/src/invite/invite-api.js +15 -3
- package/src/local-peers.js +194 -0
- package/src/mapeo-manager.js +60 -20
- package/src/mapeo-project.js +21 -3
- package/src/member-api.js +67 -10
- package/src/schema/client.js +3 -2
- package/src/sync/sync-api.js +6 -2
package/src/member-api.js
CHANGED
|
@@ -19,7 +19,12 @@ import { isHostnameIpAddress } from './lib/is-hostname-ip-address.js'
|
|
|
19
19
|
import { ErrorWithCode, getErrorMessage } from './lib/error.js'
|
|
20
20
|
import { InviteAbortedError } from './errors.js'
|
|
21
21
|
import { wsCoreReplicator } from './lib/ws-core-replicator.js'
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
BLOCKED_ROLE_ID,
|
|
24
|
+
MEMBER_ROLE_ID,
|
|
25
|
+
ROLES,
|
|
26
|
+
isRoleIdForNewInvite,
|
|
27
|
+
} from './roles.js'
|
|
23
28
|
/**
|
|
24
29
|
* @import {
|
|
25
30
|
* DeviceInfo,
|
|
@@ -57,6 +62,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
57
62
|
#getProjectName
|
|
58
63
|
#projectKey
|
|
59
64
|
#rpc
|
|
65
|
+
#makeWebsocket
|
|
60
66
|
#getReplicationStream
|
|
61
67
|
#waitForInitialSyncWithPeer
|
|
62
68
|
#dataTypes
|
|
@@ -73,6 +79,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
73
79
|
* @param {() => Promisable<undefined | string>} opts.getProjectName
|
|
74
80
|
* @param {Buffer} opts.projectKey
|
|
75
81
|
* @param {import('./local-peers.js').LocalPeers} opts.rpc
|
|
82
|
+
* @param {(url: string) => WebSocket} [opts.makeWebsocket]
|
|
76
83
|
* @param {() => ReplicationStream} opts.getReplicationStream
|
|
77
84
|
* @param {(deviceId: string, abortSignal: AbortSignal) => Promise<void>} opts.waitForInitialSyncWithPeer
|
|
78
85
|
* @param {Object} opts.dataTypes
|
|
@@ -87,6 +94,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
87
94
|
getProjectName,
|
|
88
95
|
projectKey,
|
|
89
96
|
rpc,
|
|
97
|
+
makeWebsocket = (url) => new WebSocket(url),
|
|
90
98
|
getReplicationStream,
|
|
91
99
|
waitForInitialSyncWithPeer,
|
|
92
100
|
dataTypes,
|
|
@@ -99,6 +107,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
99
107
|
this.#getProjectName = getProjectName
|
|
100
108
|
this.#projectKey = projectKey
|
|
101
109
|
this.#rpc = rpc
|
|
110
|
+
this.#makeWebsocket = makeWebsocket
|
|
102
111
|
this.#getReplicationStream = getReplicationStream
|
|
103
112
|
this.#waitForInitialSyncWithPeer = waitForInitialSyncWithPeer
|
|
104
113
|
this.#dataTypes = dataTypes
|
|
@@ -157,12 +166,17 @@ export class MemberApi extends TypedEmitter {
|
|
|
157
166
|
const projectName = project.name
|
|
158
167
|
assert(projectName, 'Project must have a name to invite people')
|
|
159
168
|
|
|
169
|
+
const projectColor = project.projectColor
|
|
170
|
+
const projectDescription = project.projectDescription
|
|
171
|
+
|
|
160
172
|
abortSignal.throwIfAborted()
|
|
161
173
|
|
|
162
174
|
const invite = {
|
|
163
175
|
inviteId,
|
|
164
176
|
projectInviteId,
|
|
165
177
|
projectName,
|
|
178
|
+
projectColor,
|
|
179
|
+
projectDescription,
|
|
166
180
|
roleName,
|
|
167
181
|
roleDescription,
|
|
168
182
|
invitorName,
|
|
@@ -186,18 +200,16 @@ export class MemberApi extends TypedEmitter {
|
|
|
186
200
|
case InviteResponse_Decision.DECISION_UNSPECIFIED:
|
|
187
201
|
return InviteResponse_Decision.REJECT
|
|
188
202
|
case InviteResponse_Decision.ACCEPT:
|
|
189
|
-
// We should assign the role locally *before* sharing the project details
|
|
190
|
-
// so that they're part of the project even if they don't receive the
|
|
191
|
-
// project details message.
|
|
192
|
-
|
|
193
|
-
await this.#roles.assignRole(deviceId, roleId)
|
|
194
|
-
|
|
195
203
|
await this.#rpc.sendProjectJoinDetails(deviceId, {
|
|
196
204
|
inviteId,
|
|
197
205
|
projectKey: this.#projectKey,
|
|
198
206
|
encryptionKeys: this.#encryptionKeys,
|
|
199
207
|
})
|
|
200
208
|
|
|
209
|
+
// Only add after we know they got the details
|
|
210
|
+
// Otherwise the joiner will be stuck unable to join
|
|
211
|
+
await this.#roles.assignRole(deviceId, roleId)
|
|
212
|
+
|
|
201
213
|
return inviteResponse.decision
|
|
202
214
|
default:
|
|
203
215
|
throw new ExhaustivenessError(inviteResponse.decision)
|
|
@@ -314,6 +326,46 @@ export class MemberApi extends TypedEmitter {
|
|
|
314
326
|
})
|
|
315
327
|
}
|
|
316
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Remove a server peer. Only works when the peer is reachable
|
|
331
|
+
*
|
|
332
|
+
* @param {string} serverDeviceId
|
|
333
|
+
* @param {object} [options]
|
|
334
|
+
* @param {boolean} [options.dangerouslyAllowInsecureConnections] Allow insecure network connections. Should only be used in tests.
|
|
335
|
+
* @returns {Promise<void>}
|
|
336
|
+
*/
|
|
337
|
+
async removeServerPeer(
|
|
338
|
+
serverDeviceId,
|
|
339
|
+
{ dangerouslyAllowInsecureConnections = false } = {}
|
|
340
|
+
) {
|
|
341
|
+
// Get device ID for URL
|
|
342
|
+
// Parse through URL to ensure end pathname if missing
|
|
343
|
+
const member = await this.getById(serverDeviceId)
|
|
344
|
+
|
|
345
|
+
if (!member.selfHostedServerDetails) {
|
|
346
|
+
throw new ErrorWithCode(
|
|
347
|
+
'DEVICE_ID_NOT_FOR_SERVER',
|
|
348
|
+
'DeviceId is not for a server peer'
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (member.role.roleId === BLOCKED_ROLE_ID) {
|
|
353
|
+
throw new ErrorWithCode('ALREADY_BLOCKED', 'Server peer already blocked')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const { baseUrl } = member.selfHostedServerDetails
|
|
357
|
+
|
|
358
|
+
// Add blocked role to project
|
|
359
|
+
await this.#roles.assignRole(serverDeviceId, BLOCKED_ROLE_ID)
|
|
360
|
+
|
|
361
|
+
// TODO: Catch fail and sync with server after
|
|
362
|
+
await this.#waitForInitialSyncWithServer({
|
|
363
|
+
baseUrl,
|
|
364
|
+
serverDeviceId,
|
|
365
|
+
dangerouslyAllowInsecureConnections,
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
317
369
|
/**
|
|
318
370
|
* @param {string} baseUrl Server base URL. Should already be validated.
|
|
319
371
|
* @returns {Promise<{ serverDeviceId: string }>}
|
|
@@ -378,7 +430,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
378
430
|
? 'ws:'
|
|
379
431
|
: 'wss:'
|
|
380
432
|
|
|
381
|
-
const websocket =
|
|
433
|
+
const websocket = this.#makeWebsocket(websocketUrl.href)
|
|
382
434
|
|
|
383
435
|
try {
|
|
384
436
|
await pEvent(websocket, 'open', { rejectionEvents: ['error'] })
|
|
@@ -400,7 +452,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
400
452
|
const onErrorPromise = pEvent(websocket, 'error')
|
|
401
453
|
|
|
402
454
|
const replicationStream = this.#getReplicationStream()
|
|
403
|
-
wsCoreReplicator(websocket, replicationStream)
|
|
455
|
+
const streamPromise = wsCoreReplicator(websocket, replicationStream)
|
|
404
456
|
|
|
405
457
|
const syncAbortController = new AbortController()
|
|
406
458
|
const syncPromise = this.#waitForInitialSyncWithPeer(
|
|
@@ -408,7 +460,11 @@ export class MemberApi extends TypedEmitter {
|
|
|
408
460
|
syncAbortController.signal
|
|
409
461
|
)
|
|
410
462
|
|
|
411
|
-
const errorEvent = await Promise.race([
|
|
463
|
+
const errorEvent = await Promise.race([
|
|
464
|
+
onErrorPromise,
|
|
465
|
+
syncPromise,
|
|
466
|
+
streamPromise,
|
|
467
|
+
])
|
|
412
468
|
|
|
413
469
|
if (errorEvent) {
|
|
414
470
|
syncAbortController.abort()
|
|
@@ -445,6 +501,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
445
501
|
result.name = deviceInfo.name
|
|
446
502
|
result.deviceType = deviceInfo.deviceType
|
|
447
503
|
result.joinedAt = deviceInfo.createdAt
|
|
504
|
+
result.selfHostedServerDetails = deviceInfo.selfHostedServerDetails
|
|
448
505
|
} catch (err) {
|
|
449
506
|
// Attempting to get someone else may throw because sync hasn't occurred or completed
|
|
450
507
|
// Only throw if attempting to get themself since the relevant information should be available
|
package/src/schema/client.js
CHANGED
|
@@ -7,9 +7,10 @@ import { jsonSchemaToDrizzleColumns as toColumns } from './schema-to-drizzle.js'
|
|
|
7
7
|
import { backlinkTable, customJson } from './utils.js'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
+
* @import { ProjectSettings } from '@comapeo/schema'
|
|
11
|
+
*
|
|
10
12
|
* @internal
|
|
11
|
-
* @typedef {
|
|
12
|
-
* @prop {string} [name]
|
|
13
|
+
* @typedef {Pick<ProjectSettings, 'name' | 'projectColor' | 'projectDescription'>} ProjectInfo
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
const projectInfoColumn =
|
package/src/sync/sync-api.js
CHANGED
|
@@ -99,12 +99,14 @@ export class SyncApi extends TypedEmitter {
|
|
|
99
99
|
#getReplicationStream
|
|
100
100
|
/** @type {Map<string, WebSocket>} */
|
|
101
101
|
#serverWebsockets = new Map()
|
|
102
|
+
#makeWebsocket
|
|
102
103
|
|
|
103
104
|
/**
|
|
104
105
|
* @param {object} opts
|
|
105
106
|
* @param {import('../core-manager/index.js').CoreManager} opts.coreManager
|
|
106
107
|
* @param {CoreOwnership} opts.coreOwnership
|
|
107
108
|
* @param {import('../roles.js').Roles} opts.roles
|
|
109
|
+
* @param {(url: string) => WebSocket} [opts.makeWebsocket]
|
|
108
110
|
* @param {() => Promise<Iterable<string>>} opts.getServerWebsocketUrls
|
|
109
111
|
* @param {() => ReplicationStream} opts.getReplicationStream
|
|
110
112
|
* @param {import('../blob-store/index.js').BlobStore} opts.blobStore
|
|
@@ -115,6 +117,7 @@ export class SyncApi extends TypedEmitter {
|
|
|
115
117
|
coreManager,
|
|
116
118
|
throttleMs = 200,
|
|
117
119
|
roles,
|
|
120
|
+
makeWebsocket = (url) => new WebSocket(url),
|
|
118
121
|
getServerWebsocketUrls,
|
|
119
122
|
getReplicationStream,
|
|
120
123
|
logger,
|
|
@@ -126,6 +129,7 @@ export class SyncApi extends TypedEmitter {
|
|
|
126
129
|
this.#coreManager = coreManager
|
|
127
130
|
this.#coreOwnership = coreOwnership
|
|
128
131
|
this.#roles = roles
|
|
132
|
+
this.#makeWebsocket = makeWebsocket
|
|
129
133
|
this.#getServerWebsocketUrls = getServerWebsocketUrls
|
|
130
134
|
this.#getReplicationStream = getReplicationStream
|
|
131
135
|
this[kSyncState] = new SyncState({
|
|
@@ -332,7 +336,7 @@ export class SyncApi extends TypedEmitter {
|
|
|
332
336
|
continue
|
|
333
337
|
}
|
|
334
338
|
|
|
335
|
-
const websocket =
|
|
339
|
+
const websocket = this.#makeWebsocket(url)
|
|
336
340
|
|
|
337
341
|
/** @param {Error} err */
|
|
338
342
|
const onWebsocketError = (err) => {
|
|
@@ -354,7 +358,7 @@ export class SyncApi extends TypedEmitter {
|
|
|
354
358
|
websocket.on('unexpected-response', onWebsocketUnexpectedResponse)
|
|
355
359
|
|
|
356
360
|
const replicationStream = this.#getReplicationStream()
|
|
357
|
-
wsCoreReplicator(websocket, replicationStream)
|
|
361
|
+
wsCoreReplicator(websocket, replicationStream).catch(noop)
|
|
358
362
|
|
|
359
363
|
this.#serverWebsockets.set(url, websocket)
|
|
360
364
|
websocket.once('close', () => {
|