@dxos/client-services 0.5.9-main.bd9c8b3 → 0.5.9-main.bdb299c
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/lib/browser/{chunk-KI7FY3ZO.mjs → chunk-366QXBJQ.mjs} +940 -523
- package/dist/lib/browser/chunk-366QXBJQ.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +13 -2
- package/dist/lib/browser/index.mjs.map +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/packlets/testing/index.mjs +10 -3
- package/dist/lib/browser/packlets/testing/index.mjs.map +1 -1
- package/dist/lib/node/{chunk-XWMOEZYI.cjs → chunk-RZCXR5W4.cjs} +975 -558
- package/dist/lib/node/chunk-RZCXR5W4.cjs.map +7 -0
- package/dist/lib/node/index.cjs +53 -42
- package/dist/lib/node/index.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/packlets/testing/index.cjs +17 -10
- package/dist/lib/node/packlets/testing/index.cjs.map +1 -1
- package/dist/types/src/packlets/identity/contacts-service.d.ts +14 -0
- package/dist/types/src/packlets/identity/contacts-service.d.ts.map +1 -0
- package/dist/types/src/packlets/identity/identity-service.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/invitations-handler.d.ts.map +1 -1
- package/dist/types/src/packlets/invitations/space-invitation-protocol.d.ts.map +1 -1
- package/dist/types/src/packlets/services/service-host.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts +10 -1
- package/dist/types/src/packlets/spaces/data-space-manager.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/data-space.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/epoch-migrations.d.ts +2 -2
- package/dist/types/src/packlets/spaces/epoch-migrations.d.ts.map +1 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts +4 -1
- package/dist/types/src/packlets/spaces/spaces-service.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/index.d.ts +1 -0
- package/dist/types/src/packlets/storage/index.d.ts.map +1 -1
- package/dist/types/src/packlets/storage/profile-archive.d.ts +14 -0
- package/dist/types/src/packlets/storage/profile-archive.d.ts.map +1 -0
- package/dist/types/src/version.d.ts +1 -1
- package/package.json +36 -36
- package/src/packlets/identity/contacts-service.ts +85 -0
- package/src/packlets/identity/identity-service.ts +28 -22
- package/src/packlets/invitations/invitations-handler.ts +13 -5
- package/src/packlets/invitations/space-invitation-protocol.ts +11 -32
- package/src/packlets/services/service-host.ts +12 -4
- package/src/packlets/spaces/data-space-manager.ts +55 -2
- package/src/packlets/spaces/data-space.ts +3 -1
- package/src/packlets/spaces/epoch-migrations.ts +57 -38
- package/src/packlets/spaces/spaces-service.ts +40 -0
- package/src/packlets/storage/index.ts +1 -0
- package/src/packlets/storage/profile-archive.ts +111 -0
- package/src/version.ts +1 -1
- package/dist/lib/browser/chunk-KI7FY3ZO.mjs.map +0 -7
- package/dist/lib/node/chunk-XWMOEZYI.cjs.map +0 -7
|
@@ -111,29 +111,35 @@ export class IdentityServiceImpl extends Resource implements IdentityService {
|
|
|
111
111
|
|
|
112
112
|
const recordedDefaultSpaceTrigger = new Trigger();
|
|
113
113
|
|
|
114
|
-
const allProcessed = safeAwaitAll(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (foundDefaultSpace) {
|
|
127
|
-
log.warn('Multiple default spaces found. Using the first one.', { duplicate: space.id });
|
|
128
|
-
return;
|
|
114
|
+
const allProcessed = safeAwaitAll(
|
|
115
|
+
dataSpaceManager.spaces.values(),
|
|
116
|
+
async (space) => {
|
|
117
|
+
if (space.state === SpaceState.CLOSED) {
|
|
118
|
+
await space.open();
|
|
119
|
+
|
|
120
|
+
// Wait until the space is either READY or REQUIRES_MIGRATION.
|
|
121
|
+
// NOTE: Space could potentially never initialize if the space data is corrupted.
|
|
122
|
+
const requiresMigration = space.stateUpdate.waitForCondition(
|
|
123
|
+
() => space.state === SpaceState.REQUIRES_MIGRATION,
|
|
124
|
+
);
|
|
125
|
+
await Promise.race([space.initializeDataPipeline(), requiresMigration]);
|
|
129
126
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
127
|
+
if (await dataSpaceManager.isDefaultSpace(space)) {
|
|
128
|
+
if (foundDefaultSpace) {
|
|
129
|
+
log.warn('Multiple default spaces found. Using the first one.', { duplicate: space.id });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
foundDefaultSpace = true;
|
|
134
|
+
await identity.updateDefaultSpace(space.id);
|
|
135
|
+
recodedDefaultSpace = true;
|
|
136
|
+
recordedDefaultSpaceTrigger.wake();
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
(err) => {
|
|
140
|
+
log.catch(err);
|
|
141
|
+
},
|
|
142
|
+
);
|
|
137
143
|
|
|
138
144
|
// Wait for all spaces to be processed or until the default space is recorded.
|
|
139
145
|
// If the timeout is reached, create a new default space.
|
|
@@ -435,11 +435,19 @@ export class InvitationsHandler {
|
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
private _logStateUpdate(invitation: Invitation, actor: any, newState: Invitation.State) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
438
|
+
if (this._isNotTerminal(newState)) {
|
|
439
|
+
log('invitation state update', {
|
|
440
|
+
actor: actor?.constructor.name,
|
|
441
|
+
newState: stateToString(newState),
|
|
442
|
+
oldState: stateToString(invitation.state),
|
|
443
|
+
});
|
|
444
|
+
} else {
|
|
445
|
+
log.info('invitation state update', {
|
|
446
|
+
actor: actor?.constructor.name,
|
|
447
|
+
newState: stateToString(newState),
|
|
448
|
+
oldState: stateToString(invitation.state),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
443
451
|
}
|
|
444
452
|
|
|
445
453
|
private _isNotTerminal(currentState: Invitation.State): boolean {
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
createAdmissionCredentials,
|
|
7
6
|
createCancelDelegatedSpaceInvitationCredential,
|
|
8
7
|
createDelegatedSpaceInvitationCredential,
|
|
9
8
|
getCredentialAssertion,
|
|
@@ -21,7 +20,6 @@ import {
|
|
|
21
20
|
SpaceNotFoundError,
|
|
22
21
|
} from '@dxos/protocols';
|
|
23
22
|
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
24
|
-
import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
25
23
|
import { SpaceMember, type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
26
24
|
import {
|
|
27
25
|
type AdmissionRequest,
|
|
@@ -73,41 +71,22 @@ export class SpaceInvitationProtocol implements InvitationProtocol {
|
|
|
73
71
|
request: AdmissionRequest,
|
|
74
72
|
guestProfile?: ProfileDocument | undefined,
|
|
75
73
|
): Promise<AdmissionResponse> {
|
|
76
|
-
invariant(this._spaceKey);
|
|
77
|
-
|
|
78
|
-
invariant(space);
|
|
79
|
-
|
|
80
|
-
invariant(request.space);
|
|
81
|
-
const { identityKey, deviceKey } = request.space;
|
|
74
|
+
invariant(this._spaceKey && request.space);
|
|
75
|
+
log('writing guest credentials', { host: this._signingContext.deviceKey, guest: request.space.deviceKey });
|
|
82
76
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this._signingContext.credentialSigner,
|
|
91
|
-
identityKey,
|
|
92
|
-
space.key,
|
|
93
|
-
space.inner.genesisFeedKey,
|
|
94
|
-
invitation.role ?? SpaceMember.Role.ADMIN,
|
|
95
|
-
space.inner.spaceState.membershipChainHeads,
|
|
96
|
-
guestProfile,
|
|
97
|
-
invitation.delegationCredentialId,
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
// TODO(dmaretskyi): Refactor.
|
|
101
|
-
invariant(credentials[0].credential);
|
|
102
|
-
const spaceMemberCredential = credentials[0].credential.credential;
|
|
103
|
-
invariant(getCredentialAssertion(spaceMemberCredential)['@type'] === 'dxos.halo.credentials.SpaceMember');
|
|
104
|
-
|
|
105
|
-
await writeMessages(space.inner.controlPipeline.writer, credentials);
|
|
77
|
+
const spaceMemberCredential = await this._spaceManager.admitMember({
|
|
78
|
+
spaceKey: this._spaceKey,
|
|
79
|
+
identityKey: request.space.identityKey,
|
|
80
|
+
role: invitation.role ?? SpaceMember.Role.ADMIN,
|
|
81
|
+
profile: guestProfile,
|
|
82
|
+
delegationCredentialId: invitation.delegationCredentialId,
|
|
83
|
+
});
|
|
106
84
|
|
|
85
|
+
const space = this._spaceManager.spaces.get(this._spaceKey);
|
|
107
86
|
return {
|
|
108
87
|
space: {
|
|
109
88
|
credential: spaceMemberCredential,
|
|
110
|
-
controlTimeframe: space
|
|
89
|
+
controlTimeframe: space?.inner.controlPipeline.state.timeframe,
|
|
111
90
|
},
|
|
112
91
|
};
|
|
113
92
|
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
createDiagnostics,
|
|
29
29
|
} from '../diagnostics';
|
|
30
30
|
import { IdentityServiceImpl, type CreateIdentityOptions } from '../identity';
|
|
31
|
+
import { ContactsServiceImpl } from '../identity/contacts-service';
|
|
31
32
|
import { InvitationsServiceImpl } from '../invitations';
|
|
32
33
|
import { Lock, type ResourceLock } from '../locks';
|
|
33
34
|
import { LoggingServiceImpl } from '../logging';
|
|
@@ -251,6 +252,11 @@ export class ClientServicesHost {
|
|
|
251
252
|
this._runtimeParams,
|
|
252
253
|
);
|
|
253
254
|
|
|
255
|
+
const dataSpaceManagerProvider = async () => {
|
|
256
|
+
await this._serviceContext.initialized.wait();
|
|
257
|
+
return this._serviceContext.dataSpaceManager!;
|
|
258
|
+
};
|
|
259
|
+
|
|
254
260
|
const identityService = new IdentityServiceImpl(
|
|
255
261
|
this._serviceContext.identityManager,
|
|
256
262
|
this._serviceContext.keyring,
|
|
@@ -262,6 +268,11 @@ export class ClientServicesHost {
|
|
|
262
268
|
this._serviceRegistry.setServices({
|
|
263
269
|
SystemService: this._systemService,
|
|
264
270
|
IdentityService: identityService,
|
|
271
|
+
ContactsService: new ContactsServiceImpl(
|
|
272
|
+
this._serviceContext.identityManager,
|
|
273
|
+
this._serviceContext.spaceManager,
|
|
274
|
+
dataSpaceManagerProvider,
|
|
275
|
+
),
|
|
265
276
|
|
|
266
277
|
InvitationsService: new InvitationsServiceImpl(this._serviceContext.invitationsManager),
|
|
267
278
|
|
|
@@ -270,10 +281,7 @@ export class ClientServicesHost {
|
|
|
270
281
|
SpacesService: new SpacesServiceImpl(
|
|
271
282
|
this._serviceContext.identityManager,
|
|
272
283
|
this._serviceContext.spaceManager,
|
|
273
|
-
|
|
274
|
-
await this._serviceContext.initialized.wait();
|
|
275
|
-
return this._serviceContext.dataSpaceManager!;
|
|
276
|
-
},
|
|
284
|
+
dataSpaceManagerProvider,
|
|
277
285
|
),
|
|
278
286
|
|
|
279
287
|
DataService: this._serviceContext.echoHost.dataService,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getCredentialAssertion,
|
|
12
12
|
type CredentialSigner,
|
|
13
13
|
type DelegateInvitationCredential,
|
|
14
|
+
createAdmissionCredentials,
|
|
14
15
|
type MemberInfo,
|
|
15
16
|
} from '@dxos/credentials';
|
|
16
17
|
import { convertLegacyReferences, findInlineObjectOfType, type EchoHost } from '@dxos/echo-db';
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
type SpaceProtocol,
|
|
23
24
|
type SpaceProtocolSession,
|
|
24
25
|
} from '@dxos/echo-pipeline';
|
|
26
|
+
import { CredentialServerExtension } from '@dxos/echo-pipeline';
|
|
25
27
|
import {
|
|
26
28
|
LEGACY_TYPE_PROPERTIES,
|
|
27
29
|
SpaceDocVersion,
|
|
@@ -30,12 +32,12 @@ import {
|
|
|
30
32
|
type SpaceDoc,
|
|
31
33
|
} from '@dxos/echo-protocol';
|
|
32
34
|
import { TYPE_PROPERTIES, generateEchoId, getTypeReference } from '@dxos/echo-schema';
|
|
33
|
-
import { type FeedStore } from '@dxos/feed-store';
|
|
35
|
+
import { type FeedStore, writeMessages } from '@dxos/feed-store';
|
|
34
36
|
import { invariant } from '@dxos/invariant';
|
|
35
37
|
import { type Keyring } from '@dxos/keyring';
|
|
36
38
|
import { PublicKey } from '@dxos/keys';
|
|
37
39
|
import { log } from '@dxos/log';
|
|
38
|
-
import { trace as Trace } from '@dxos/protocols';
|
|
40
|
+
import { trace as Trace, AlreadyJoinedError } from '@dxos/protocols';
|
|
39
41
|
import { Invitation, SpaceState } from '@dxos/protocols/proto/dxos/client/services';
|
|
40
42
|
import { type FeedMessage } from '@dxos/protocols/proto/dxos/echo/feed';
|
|
41
43
|
import { type SpaceMetadata } from '@dxos/protocols/proto/dxos/echo/metadata';
|
|
@@ -84,6 +86,14 @@ export type AcceptSpaceOptions = {
|
|
|
84
86
|
dataTimeframe?: Timeframe;
|
|
85
87
|
};
|
|
86
88
|
|
|
89
|
+
export type AdmitMemberOptions = {
|
|
90
|
+
spaceKey: PublicKey;
|
|
91
|
+
identityKey: PublicKey;
|
|
92
|
+
role: SpaceMember.Role;
|
|
93
|
+
profile?: ProfileDocument;
|
|
94
|
+
delegationCredentialId?: PublicKey;
|
|
95
|
+
};
|
|
96
|
+
|
|
87
97
|
export type DataSpaceManagerRuntimeParams = {
|
|
88
98
|
spaceMemberPresenceAnnounceInterval?: number;
|
|
89
99
|
spaceMemberPresenceOfflineTimeout?: number;
|
|
@@ -287,6 +297,35 @@ export class DataSpaceManager {
|
|
|
287
297
|
return space;
|
|
288
298
|
}
|
|
289
299
|
|
|
300
|
+
async admitMember(options: AdmitMemberOptions): Promise<Credential> {
|
|
301
|
+
const space = this._spaceManager.spaces.get(options.spaceKey);
|
|
302
|
+
invariant(space);
|
|
303
|
+
|
|
304
|
+
if (space.spaceState.getMemberRole(options.identityKey) !== SpaceMember.Role.REMOVED) {
|
|
305
|
+
throw new AlreadyJoinedError();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// TODO(burdon): Check if already admitted.
|
|
309
|
+
const credentials: FeedMessage.Payload[] = await createAdmissionCredentials(
|
|
310
|
+
this._signingContext.credentialSigner,
|
|
311
|
+
options.identityKey,
|
|
312
|
+
space.key,
|
|
313
|
+
space.genesisFeedKey,
|
|
314
|
+
options.role,
|
|
315
|
+
space.spaceState.membershipChainHeads,
|
|
316
|
+
options.profile,
|
|
317
|
+
options.delegationCredentialId,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// TODO(dmaretskyi): Refactor.
|
|
321
|
+
invariant(credentials[0].credential);
|
|
322
|
+
const spaceMemberCredential = credentials[0].credential.credential;
|
|
323
|
+
invariant(getCredentialAssertion(spaceMemberCredential)['@type'] === 'dxos.halo.credentials.SpaceMember');
|
|
324
|
+
await writeMessages(space.controlPipeline.writer, credentials);
|
|
325
|
+
|
|
326
|
+
return spaceMemberCredential;
|
|
327
|
+
}
|
|
328
|
+
|
|
290
329
|
/**
|
|
291
330
|
* Wait until the space data pipeline is fully initialized.
|
|
292
331
|
* Used by invitation handler.
|
|
@@ -302,6 +341,19 @@ export class DataSpaceManager {
|
|
|
302
341
|
);
|
|
303
342
|
}
|
|
304
343
|
|
|
344
|
+
public async requestSpaceAdmissionCredential(spaceKey: PublicKey): Promise<Credential> {
|
|
345
|
+
return this._spaceManager.requestSpaceAdmissionCredential({
|
|
346
|
+
spaceKey,
|
|
347
|
+
identityKey: this._signingContext.identityKey,
|
|
348
|
+
timeout: 15_000,
|
|
349
|
+
swarmIdentity: {
|
|
350
|
+
peerKey: this._signingContext.deviceKey,
|
|
351
|
+
credentialProvider: createAuthProvider(this._signingContext.credentialSigner),
|
|
352
|
+
credentialAuthenticator: async () => true,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
305
357
|
private async _constructSpace(metadata: SpaceMetadata) {
|
|
306
358
|
log('construct space', { metadata });
|
|
307
359
|
const gossip = new Gossip({
|
|
@@ -331,6 +383,7 @@ export class DataSpaceManager {
|
|
|
331
383
|
credentialAuthenticator: deferFunction(() => dataSpace.authVerifier.verifier),
|
|
332
384
|
},
|
|
333
385
|
onAuthorizedConnection: (session) => {
|
|
386
|
+
session.addExtension('dxos.mesh.teleport.admission-discovery', new CredentialServerExtension(space));
|
|
334
387
|
session.addExtension(
|
|
335
388
|
'dxos.mesh.teleport.gossip',
|
|
336
389
|
gossip.createExtension({ remotePeerId: session.remotePeerId }),
|
|
@@ -413,6 +413,8 @@ export class DataSpace {
|
|
|
413
413
|
// TODO(dmaretskyi): Close roots.
|
|
414
414
|
// TODO(dmaretskyi): How do we handle changing to the next EPOCH?
|
|
415
415
|
const root = await this._echoHost.openSpaceRoot(handle.url);
|
|
416
|
+
|
|
417
|
+
// NOTE: Make sure this assignment happens synchronously together with the state change.
|
|
416
418
|
this._databaseRoot = root;
|
|
417
419
|
if (root.getVersion() !== SpaceDocVersion.CURRENT) {
|
|
418
420
|
if (this._state !== SpaceState.REQUIRES_MIGRATION) {
|
|
@@ -454,7 +456,7 @@ export class DataSpace {
|
|
|
454
456
|
}
|
|
455
457
|
|
|
456
458
|
const { newRoot } = await runEpochMigration(ctx, {
|
|
457
|
-
|
|
459
|
+
echoHost: this._echoHost,
|
|
458
460
|
spaceId: this.id,
|
|
459
461
|
spaceKey: this.key,
|
|
460
462
|
migration: options.migration,
|
|
@@ -2,27 +2,25 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import type { Repo, AutomergeUrl } from '@dxos/automerge/automerge-repo';
|
|
8
|
-
import { cancelWithContext, type Context } from '@dxos/context';
|
|
5
|
+
import type { AutomergeUrl } from '@dxos/automerge/automerge-repo';
|
|
6
|
+
import { type Context } from '@dxos/context';
|
|
9
7
|
import {
|
|
10
8
|
convertLegacyReferences,
|
|
11
9
|
convertLegacySpaceRootDoc,
|
|
12
10
|
findInlineObjectOfType,
|
|
13
11
|
migrateDocument,
|
|
12
|
+
type EchoHost,
|
|
14
13
|
} from '@dxos/echo-db';
|
|
15
|
-
import {
|
|
16
|
-
import type { SpaceDoc } from '@dxos/echo-protocol';
|
|
14
|
+
import { SpaceDocVersion, type SpaceDoc } from '@dxos/echo-protocol';
|
|
17
15
|
import { TYPE_PROPERTIES } from '@dxos/echo-schema';
|
|
18
16
|
import { invariant } from '@dxos/invariant';
|
|
19
17
|
import type { PublicKey, SpaceId } from '@dxos/keys';
|
|
20
18
|
import { log } from '@dxos/log';
|
|
21
19
|
import { CreateEpochRequest } from '@dxos/protocols/proto/dxos/client/services';
|
|
22
|
-
import { assignDeep } from '@dxos/util';
|
|
23
20
|
|
|
24
21
|
export type MigrationContext = {
|
|
25
|
-
|
|
22
|
+
echoHost: EchoHost;
|
|
23
|
+
|
|
26
24
|
spaceId: SpaceId;
|
|
27
25
|
/**
|
|
28
26
|
* @deprecated Remove.
|
|
@@ -41,30 +39,34 @@ export type MigrationResult = {
|
|
|
41
39
|
newRoot?: string;
|
|
42
40
|
};
|
|
43
41
|
|
|
42
|
+
const LOAD_DOC_TIMEOUT = 10_000;
|
|
43
|
+
|
|
44
44
|
export const runEpochMigration = async (ctx: Context, context: MigrationContext): Promise<MigrationResult> => {
|
|
45
45
|
switch (context.migration) {
|
|
46
46
|
case CreateEpochRequest.Migration.INIT_AUTOMERGE: {
|
|
47
|
-
const document = context.
|
|
48
|
-
await context.
|
|
47
|
+
const document = context.echoHost.createDoc();
|
|
48
|
+
await context.echoHost.flush();
|
|
49
49
|
return { newRoot: document.url };
|
|
50
50
|
}
|
|
51
51
|
case CreateEpochRequest.Migration.PRUNE_AUTOMERGE_ROOT_HISTORY: {
|
|
52
52
|
if (!context.currentRoot) {
|
|
53
53
|
throw new Error('Space does not have an automerge root');
|
|
54
54
|
}
|
|
55
|
-
const rootHandle = context.
|
|
56
|
-
|
|
55
|
+
const rootHandle = await context.echoHost.loadDoc(ctx, context.currentRoot as AutomergeUrl, {
|
|
56
|
+
timeout: LOAD_DOC_TIMEOUT,
|
|
57
|
+
});
|
|
57
58
|
|
|
58
|
-
const newRoot = context.
|
|
59
|
-
await context.
|
|
59
|
+
const newRoot = context.echoHost.createDoc(rootHandle.docSync());
|
|
60
|
+
await context.echoHost.flush();
|
|
60
61
|
return { newRoot: newRoot.url };
|
|
61
62
|
}
|
|
62
63
|
case CreateEpochRequest.Migration.FRAGMENT_AUTOMERGE_ROOT: {
|
|
63
64
|
log.info('Fragmenting');
|
|
64
65
|
|
|
65
66
|
const currentRootUrl = context.currentRoot;
|
|
66
|
-
const rootHandle = context.
|
|
67
|
-
|
|
67
|
+
const rootHandle = await context.echoHost.loadDoc<SpaceDoc>(ctx, currentRootUrl as any, {
|
|
68
|
+
timeout: LOAD_DOC_TIMEOUT,
|
|
69
|
+
});
|
|
68
70
|
|
|
69
71
|
// Find properties object.
|
|
70
72
|
const objects = Object.entries((rootHandle.docSync() as SpaceDoc).objects!);
|
|
@@ -73,48 +75,65 @@ export const runEpochMigration = async (ctx: Context, context: MigrationContext)
|
|
|
73
75
|
invariant(properties, 'Properties not found');
|
|
74
76
|
|
|
75
77
|
// Create a new space doc with the properties object.
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
+
const newRoot = context.echoHost.createDoc({
|
|
79
|
+
...rootHandle.docSync(),
|
|
80
|
+
objects: Object.fromEntries([properties]),
|
|
81
|
+
});
|
|
78
82
|
invariant(typeof newRoot.url === 'string' && newRoot.url.length > 0);
|
|
79
83
|
|
|
80
84
|
// Create new automerge documents for all objects.
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
const newLinks: [string, AutomergeUrl][] = [];
|
|
86
|
+
for (const [id, objData] of otherObjects) {
|
|
87
|
+
const handle = context.echoHost.createDoc<SpaceDoc>({
|
|
88
|
+
version: SpaceDocVersion.CURRENT,
|
|
89
|
+
access: {
|
|
90
|
+
spaceKey: context.spaceKey.toHex(),
|
|
91
|
+
},
|
|
92
|
+
objects: {
|
|
93
|
+
[id]: objData,
|
|
94
|
+
},
|
|
88
95
|
});
|
|
96
|
+
newLinks.push([id, handle.url]);
|
|
97
|
+
}
|
|
98
|
+
newRoot.change((doc: SpaceDoc) => {
|
|
99
|
+
doc.links ??= {};
|
|
100
|
+
for (const [id, url] of newLinks) {
|
|
101
|
+
doc.links[id] = url;
|
|
102
|
+
}
|
|
89
103
|
});
|
|
90
104
|
|
|
91
|
-
await context.
|
|
105
|
+
await context.echoHost.flush();
|
|
92
106
|
return {
|
|
93
107
|
newRoot: newRoot.url,
|
|
94
108
|
};
|
|
95
109
|
}
|
|
96
110
|
case CreateEpochRequest.Migration.MIGRATE_REFERENCES_TO_DXN: {
|
|
97
111
|
const currentRootUrl = context.currentRoot;
|
|
98
|
-
const rootHandle = context.
|
|
99
|
-
|
|
112
|
+
const rootHandle = await context.echoHost.loadDoc<SpaceDoc>(ctx, currentRootUrl as any, {
|
|
113
|
+
timeout: LOAD_DOC_TIMEOUT,
|
|
114
|
+
});
|
|
100
115
|
invariant(rootHandle.docSync(), 'Root doc not found');
|
|
101
116
|
|
|
102
117
|
const newRootContent = await convertLegacySpaceRootDoc(structuredClone(rootHandle.docSync()!));
|
|
103
118
|
|
|
104
119
|
for (const [id, url] of Object.entries(newRootContent.links ?? {})) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
try {
|
|
121
|
+
const handle = await context.echoHost.loadDoc(ctx, url as any, { timeout: LOAD_DOC_TIMEOUT });
|
|
122
|
+
invariant(handle.docSync());
|
|
123
|
+
const newDoc = await convertLegacyReferences(structuredClone(handle.docSync()!));
|
|
124
|
+
const migratedDoc = migrateDocument(handle.docSync(), newDoc);
|
|
125
|
+
const newHandle = context.echoHost.createDoc(migratedDoc, { preserveHistory: true });
|
|
126
|
+
newRootContent.links![id] = newHandle.url;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
log.warn('Failed to migrate reference', { id, url, error: err });
|
|
129
|
+
delete newRootContent.links![id];
|
|
130
|
+
}
|
|
112
131
|
}
|
|
113
132
|
|
|
114
133
|
const migratedRoot = migrateDocument(rootHandle.docSync(), newRootContent);
|
|
115
|
-
const newRoot = context.
|
|
134
|
+
const newRoot = context.echoHost.createDoc(migratedRoot, { preserveHistory: true });
|
|
116
135
|
|
|
117
|
-
await context.
|
|
136
|
+
await context.echoHost.flush();
|
|
118
137
|
return {
|
|
119
138
|
newRoot: newRoot.url,
|
|
120
139
|
};
|
|
@@ -124,7 +143,7 @@ export const runEpochMigration = async (ctx: Context, context: MigrationContext)
|
|
|
124
143
|
invariant(context.newAutomergeRoot);
|
|
125
144
|
|
|
126
145
|
// Defensive programming - it should be the responsibility of the caller to flush the new root.
|
|
127
|
-
await context.
|
|
146
|
+
await context.echoHost.flush();
|
|
128
147
|
return {
|
|
129
148
|
newRoot: context.newAutomergeRoot,
|
|
130
149
|
};
|
|
@@ -30,6 +30,10 @@ import {
|
|
|
30
30
|
type UpdateSpaceRequest,
|
|
31
31
|
type WriteCredentialsRequest,
|
|
32
32
|
type UpdateMemberRoleRequest,
|
|
33
|
+
type AdmitContactRequest,
|
|
34
|
+
type ContactAdmission,
|
|
35
|
+
type JoinSpaceResponse,
|
|
36
|
+
type JoinBySpaceKeyRequest,
|
|
33
37
|
type CreateEpochResponse,
|
|
34
38
|
} from '@dxos/protocols/proto/dxos/client/services';
|
|
35
39
|
import { type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
|
|
@@ -226,6 +230,40 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
226
230
|
return { epochCredential: credential ?? undefined };
|
|
227
231
|
}
|
|
228
232
|
|
|
233
|
+
async admitContact(request: AdmitContactRequest): Promise<void> {
|
|
234
|
+
const dataSpaceManager = await this._getDataSpaceManager();
|
|
235
|
+
await dataSpaceManager.admitMember({
|
|
236
|
+
spaceKey: request.spaceKey,
|
|
237
|
+
identityKey: request.contact.identityKey,
|
|
238
|
+
role: request.role,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async joinBySpaceKey({ spaceKey }: JoinBySpaceKeyRequest): Promise<JoinSpaceResponse> {
|
|
243
|
+
const dataSpaceManager = await this._getDataSpaceManager();
|
|
244
|
+
const credential = await dataSpaceManager.requestSpaceAdmissionCredential(spaceKey);
|
|
245
|
+
return this._joinByAdmission({ credential });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private async _joinByAdmission({ credential }: ContactAdmission): Promise<JoinSpaceResponse> {
|
|
249
|
+
const assertion = getCredentialAssertion(credential);
|
|
250
|
+
invariant(assertion['@type'] === 'dxos.halo.credentials.SpaceMember', 'Invalid credential');
|
|
251
|
+
const myIdentity = this._identityManager.identity;
|
|
252
|
+
invariant(myIdentity && credential.subject.id.equals(myIdentity.identityKey));
|
|
253
|
+
|
|
254
|
+
const dataSpaceManager = await this._getDataSpaceManager();
|
|
255
|
+
let dataSpace = dataSpaceManager.spaces.get(assertion.spaceKey);
|
|
256
|
+
if (!dataSpace) {
|
|
257
|
+
dataSpace = await dataSpaceManager.acceptSpace({
|
|
258
|
+
spaceKey: assertion.spaceKey,
|
|
259
|
+
genesisFeedKey: assertion.genesisFeedKey,
|
|
260
|
+
});
|
|
261
|
+
await myIdentity.controlPipeline.writer.write({ credential: { credential } });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { space: this._serializeSpace(dataSpace) };
|
|
265
|
+
}
|
|
266
|
+
|
|
229
267
|
private _serializeSpace(space: DataSpace): Space {
|
|
230
268
|
return {
|
|
231
269
|
id: space.id,
|
|
@@ -246,6 +284,8 @@ export class SpacesServiceImpl implements SpacesService {
|
|
|
246
284
|
currentDataTimeframe: undefined,
|
|
247
285
|
targetDataTimeframe: undefined,
|
|
248
286
|
totalDataTimeframe: undefined,
|
|
287
|
+
|
|
288
|
+
spaceRootUrl: space.databaseRoot?.url,
|
|
249
289
|
},
|
|
250
290
|
members: Array.from(space.inner.spaceState.members.values()).map((member) => {
|
|
251
291
|
const peers = space.presence.getPeersOnline().filter(({ identityKey }) => identityKey.equals(member.key));
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { cbor } from '@dxos/automerge/automerge-repo';
|
|
6
|
+
import { invariant } from '@dxos/invariant';
|
|
7
|
+
import type { LevelDB } from '@dxos/kv-store';
|
|
8
|
+
import { log } from '@dxos/log';
|
|
9
|
+
import { ProfileArchiveEntryType, type ProfileArchive } from '@dxos/protocols';
|
|
10
|
+
import type { Storage } from '@dxos/random-access-storage';
|
|
11
|
+
import { arrayToBuffer } from '@dxos/util';
|
|
12
|
+
|
|
13
|
+
export const encodeProfileArchive = (profile: ProfileArchive): Uint8Array => cbor.encode(profile);
|
|
14
|
+
|
|
15
|
+
export const decodeProfileArchive = (data: Uint8Array): ProfileArchive => cbor.decode(data);
|
|
16
|
+
|
|
17
|
+
export const exportProfileData = async ({
|
|
18
|
+
storage,
|
|
19
|
+
level,
|
|
20
|
+
}: {
|
|
21
|
+
storage: Storage;
|
|
22
|
+
level: LevelDB;
|
|
23
|
+
}): Promise<ProfileArchive> => {
|
|
24
|
+
const archive: ProfileArchive = { storage: [], meta: { timestamp: new Date().toISOString() } };
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
const directory = await storage.createDirectory();
|
|
28
|
+
const files = await directory.list();
|
|
29
|
+
|
|
30
|
+
log.info('begin exporting files', { count: files.length });
|
|
31
|
+
for (const filename of files) {
|
|
32
|
+
const file = await directory.getOrCreateFile(filename);
|
|
33
|
+
const { size } = await file.stat();
|
|
34
|
+
const data = await file.read(0, size);
|
|
35
|
+
archive.storage.push({
|
|
36
|
+
type: ProfileArchiveEntryType.FILE,
|
|
37
|
+
key: filename,
|
|
38
|
+
value: data,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
log.info('done exporting files', { count: files.length });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
log.info('begin exporting kv pairs');
|
|
46
|
+
const iter = await level.iterator<Uint8Array, Uint8Array>({ keyEncoding: 'binary', valueEncoding: 'binary' });
|
|
47
|
+
let count = 0;
|
|
48
|
+
for await (const [key, value] of iter) {
|
|
49
|
+
archive.storage.push({
|
|
50
|
+
type: ProfileArchiveEntryType.KEY_VALUE,
|
|
51
|
+
key,
|
|
52
|
+
value,
|
|
53
|
+
});
|
|
54
|
+
count++;
|
|
55
|
+
}
|
|
56
|
+
log.info('done exporting kv pairs', { count });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return archive;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const importProfileData = async (
|
|
63
|
+
{
|
|
64
|
+
storage,
|
|
65
|
+
level,
|
|
66
|
+
}: {
|
|
67
|
+
storage: Storage;
|
|
68
|
+
level: LevelDB;
|
|
69
|
+
},
|
|
70
|
+
archive: ProfileArchive,
|
|
71
|
+
): Promise<void> => {
|
|
72
|
+
let batch = level.batch();
|
|
73
|
+
|
|
74
|
+
let count = 0;
|
|
75
|
+
for (const entry of archive.storage) {
|
|
76
|
+
switch (entry.type) {
|
|
77
|
+
case ProfileArchiveEntryType.FILE: {
|
|
78
|
+
const directory = await storage.createDirectory();
|
|
79
|
+
invariant(typeof entry.key === 'string', 'Invalid key type');
|
|
80
|
+
const file = await directory.getOrCreateFile(entry.key);
|
|
81
|
+
invariant(entry.value instanceof Uint8Array, 'Invalid value type');
|
|
82
|
+
await file.write(0, arrayToBuffer(entry.value));
|
|
83
|
+
await file.close();
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case ProfileArchiveEntryType.KEY_VALUE: {
|
|
87
|
+
invariant(entry.key instanceof Uint8Array, 'Invalid key type');
|
|
88
|
+
invariant(entry.value instanceof Uint8Array, 'Invalid value type');
|
|
89
|
+
batch.put(entry.key, entry.value, { keyEncoding: 'binary', valueEncoding: 'binary' });
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
throw new Error(`Invalid entry type: ${entry.type}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (++count % 1000 === 0) {
|
|
97
|
+
// Apparently indexedDB can't handle big batches.
|
|
98
|
+
await batch.write();
|
|
99
|
+
batch = level.batch();
|
|
100
|
+
|
|
101
|
+
log.info('importing', {
|
|
102
|
+
count,
|
|
103
|
+
total: archive.storage.length,
|
|
104
|
+
progress: `${((count / archive.storage.length) * 100).toFixed()}%`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
log.info('committing changes..');
|
|
110
|
+
await batch.write();
|
|
111
|
+
};
|