@dxos/echo-db 2.33.1-dev.4e1146b1 → 2.33.1-dev.66a9dc1b

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 (75) hide show
  1. package/dist/src/database/data-mirror.js +2 -2
  2. package/dist/src/database/data-mirror.js.map +1 -1
  3. package/dist/src/database/item-demuxer.test.js +2 -2
  4. package/dist/src/database/item-demuxer.test.js.map +1 -1
  5. package/dist/src/echo.test.js +70 -0
  6. package/dist/src/echo.test.js.map +1 -1
  7. package/dist/src/invitations/greeting-initiator.d.ts +1 -4
  8. package/dist/src/invitations/greeting-initiator.d.ts.map +1 -1
  9. package/dist/src/invitations/greeting-initiator.js +1 -7
  10. package/dist/src/invitations/greeting-initiator.js.map +1 -1
  11. package/dist/src/invitations/halo-recovery-initiator.d.ts.map +1 -1
  12. package/dist/src/invitations/halo-recovery-initiator.js +2 -2
  13. package/dist/src/invitations/halo-recovery-initiator.js.map +1 -1
  14. package/dist/src/invitations/offline-invitation-claimer.d.ts.map +1 -1
  15. package/dist/src/invitations/offline-invitation-claimer.js +2 -2
  16. package/dist/src/invitations/offline-invitation-claimer.js.map +1 -1
  17. package/dist/src/parties/authenticator.d.ts +5 -0
  18. package/dist/src/parties/authenticator.d.ts.map +1 -0
  19. package/dist/src/parties/authenticator.js +27 -0
  20. package/dist/src/parties/authenticator.js.map +1 -0
  21. package/dist/src/parties/party-core.d.ts.map +1 -1
  22. package/dist/src/parties/party-core.js +8 -3
  23. package/dist/src/parties/party-core.js.map +1 -1
  24. package/dist/src/parties/party-core.test.js +136 -5
  25. package/dist/src/parties/party-core.test.js.map +1 -1
  26. package/dist/src/parties/party-factory.d.ts.map +1 -1
  27. package/dist/src/parties/party-factory.js +1 -5
  28. package/dist/src/parties/party-factory.js.map +1 -1
  29. package/dist/src/parties/party-internal.d.ts.map +1 -1
  30. package/dist/src/parties/party-internal.js +6 -4
  31. package/dist/src/parties/party-internal.js.map +1 -1
  32. package/dist/src/parties/party-manager.test.js +4 -4
  33. package/dist/src/parties/party-manager.test.js.map +1 -1
  34. package/dist/src/pipeline/party-feed-provider.d.ts +5 -1
  35. package/dist/src/pipeline/party-feed-provider.d.ts.map +1 -1
  36. package/dist/src/pipeline/party-feed-provider.js +36 -13
  37. package/dist/src/pipeline/party-feed-provider.js.map +1 -1
  38. package/dist/src/pipeline/party-processor.d.ts +3 -4
  39. package/dist/src/pipeline/party-processor.d.ts.map +1 -1
  40. package/dist/src/pipeline/party-processor.js +8 -14
  41. package/dist/src/pipeline/party-processor.js.map +1 -1
  42. package/dist/src/pipeline/party-processor.test.js +8 -8
  43. package/dist/src/pipeline/party-processor.test.js.map +1 -1
  44. package/dist/src/pipeline/party-protocol-factory.d.ts +11 -0
  45. package/dist/src/pipeline/party-protocol-factory.d.ts.map +1 -1
  46. package/dist/src/pipeline/party-protocol-factory.js +2 -1
  47. package/dist/src/pipeline/party-protocol-factory.js.map +1 -1
  48. package/dist/src/pipeline/pipeline.js +1 -1
  49. package/dist/src/pipeline/pipeline.js.map +1 -1
  50. package/dist/src/pipeline/pipeline.test.js +3 -3
  51. package/dist/src/pipeline/pipeline.test.js.map +1 -1
  52. package/dist/src/util/persistant-ram-storage.d.ts.map +1 -1
  53. package/dist/src/util/persistant-ram-storage.js +1 -1
  54. package/dist/src/util/persistant-ram-storage.js.map +1 -1
  55. package/dist/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +17 -17
  57. package/src/database/data-mirror.ts +2 -2
  58. package/src/database/item-demuxer.test.ts +3 -3
  59. package/src/echo.test.ts +100 -1
  60. package/src/invitations/greeting-initiator.ts +1 -26
  61. package/src/invitations/halo-recovery-initiator.ts +4 -4
  62. package/src/invitations/offline-invitation-claimer.ts +4 -4
  63. package/src/parties/authenticator.ts +31 -0
  64. package/src/parties/party-core.test.ts +233 -10
  65. package/src/parties/party-core.ts +9 -3
  66. package/src/parties/party-factory.ts +1 -6
  67. package/src/parties/party-internal.ts +15 -6
  68. package/src/parties/party-manager.test.ts +5 -5
  69. package/src/pipeline/party-feed-provider.ts +40 -14
  70. package/src/pipeline/party-processor.test.ts +8 -8
  71. package/src/pipeline/party-processor.ts +8 -18
  72. package/src/pipeline/party-protocol-factory.ts +1 -1
  73. package/src/pipeline/pipeline.test.ts +4 -4
  74. package/src/pipeline/pipeline.ts +1 -1
  75. package/src/util/persistant-ram-storage.ts +3 -3
package/src/echo.test.ts CHANGED
@@ -7,7 +7,7 @@ import debug from 'debug';
7
7
  import expect from 'expect';
8
8
  import { it as test } from 'mocha';
9
9
 
10
- import { latch, waitForCondition } from '@dxos/async';
10
+ import { latch, promiseTimeout, waitForCondition } from '@dxos/async';
11
11
  import { defaultSecretProvider, defaultSecretValidator } from '@dxos/credentials';
12
12
  import { generateSeedPhrase, keyPairFromSeedPhrase, createKeyPair } from '@dxos/crypto';
13
13
  import { ObjectModel } from '@dxos/object-model';
@@ -329,6 +329,105 @@ describe('ECHO', () => {
329
329
  expect(a.queryParties().value[1].key).toEqual(b.queryParties().value[1].key);
330
330
  }).timeout(10_000);
331
331
 
332
+ test('Mutations from another device', async () => {
333
+ const a = await setup({ createProfile: true });
334
+ const b = await setup();
335
+
336
+ await a.createParty();
337
+
338
+ const invitation = await a.halo.createInvitation(defaultInvitationAuthenticator);
339
+ await b.halo.join(invitation, defaultSecretProvider);
340
+
341
+ // Check the initial party is opened.
342
+ await waitForCondition(() => b.queryParties().value.length === 1, 1000);
343
+
344
+ const partyA = a.queryParties().value[0];
345
+ await partyA.open();
346
+ const partyB = b.queryParties().value[0];
347
+ await partyB.open();
348
+
349
+ {
350
+ // Subscribe to Item updates on B.
351
+ const selection = partyA.database.select({ type: 'example:item/test' }).exec();
352
+ const updated = selection.update.waitFor(result => result.entities.length > 0);
353
+
354
+ // Create a new Item on A.
355
+ const itemB = await partyB.database
356
+ .createItem({ model: ObjectModel, type: 'example:item/test' }) as Item<any>;
357
+ log(`A created ${itemB.id}`);
358
+
359
+ // Now wait to see it on B.
360
+ await updated;
361
+ log(`B has ${itemB.id}`);
362
+
363
+ expect(selection.entities[0].id).toEqual(itemB.id);
364
+ }
365
+
366
+ await a.close();
367
+ await b.close();
368
+
369
+ await a.open();
370
+
371
+ {
372
+ const partyA = a.queryParties().first;
373
+
374
+ expect(partyA.database.select({ type: 'example:item/test' }).exec().entities.length > 0).toEqual(true);
375
+ }
376
+
377
+ }).timeout(10_000);
378
+
379
+ test('3 devices', async () => {
380
+ const a = await setup({ createProfile: true });
381
+ const b = await setup();
382
+
383
+ await a.createParty();
384
+
385
+ await b.halo.join(await a.halo.createInvitation(defaultInvitationAuthenticator), defaultSecretProvider);
386
+
387
+ // Check the initial party is opened.
388
+ await waitForCondition(() => b.queryParties().value.length === 1, 1000);
389
+
390
+ const partyA = a.queryParties().value[0];
391
+ await partyA.open();
392
+ const partyB = b.queryParties().value[0];
393
+ await partyB.open();
394
+
395
+ {
396
+ // Subscribe to Item updates on B.
397
+ const selection = partyA.database.select({ type: 'example:item/test' }).exec();
398
+ const updated = selection.update.waitFor(result => result.entities.length > 0);
399
+
400
+ // Create a new Item on A.
401
+ const itemB = await partyB.database
402
+ .createItem({ model: ObjectModel, type: 'example:item/test' }) as Item<any>;
403
+ log(`A created ${itemB.id}`);
404
+
405
+ // Now wait to see it on B.
406
+ await updated;
407
+ log(`B has ${itemB.id}`);
408
+
409
+ expect(selection.entities[0].id).toEqual(itemB.id);
410
+ }
411
+
412
+ await a.close();
413
+ await b.close();
414
+
415
+ await a.open();
416
+
417
+ {
418
+ const partyA = a.queryParties().first;
419
+
420
+ expect(partyA.database.select({ type: 'example:item/test' }).exec().entities.length > 0).toEqual(true);
421
+ }
422
+
423
+ const c = await setup();
424
+ await c.halo.join(await a.halo.createInvitation(defaultInvitationAuthenticator), defaultSecretProvider);
425
+ await waitForCondition(() => c.queryParties().value.length === 1, 1000);
426
+ const partyC = c.queryParties().first;
427
+
428
+ await promiseTimeout(partyC.database.waitForItem({ type: 'example:item/test' }), 1000, new Error('timeout'));
429
+ }).timeout(10_000);
430
+
332
431
  test('Two users, two devices each', async () => {
333
432
  const a1 = await setup({ createProfile: true });
334
433
  const a2 = await setup();
@@ -8,7 +8,6 @@ import debug from 'debug';
8
8
  import { waitForEvent } from '@dxos/async';
9
9
  import {
10
10
  createEnvelopeMessage,
11
- createFeedAdmitMessage,
12
11
  createGreetingBeginMessage,
13
12
  createGreetingFinishMessage,
14
13
  createGreetingHandshakeMessage,
@@ -23,7 +22,6 @@ import {
23
22
  ERR_GREET_CONNECTED_TO_SWARM_TIMEOUT
24
23
  } from '@dxos/credentials';
25
24
  import { keyToString, PublicKey } from '@dxos/crypto';
26
- import { PartyKey } from '@dxos/echo-protocol';
27
25
  import { FullyConnectedTopology, NetworkManager } from '@dxos/network-manager';
28
26
 
29
27
  import { Identity } from '../halo';
@@ -54,8 +52,7 @@ export class GreetingInitiator {
54
52
  constructor (
55
53
  private readonly _networkManager: NetworkManager,
56
54
  private readonly _identity: Identity,
57
- private readonly _invitationDescriptor: InvitationDescriptor,
58
- private readonly _feedInitializer: (partyKey: PartyKey) => Promise<PublicKey>
55
+ private readonly _invitationDescriptor: InvitationDescriptor
59
56
  ) {
60
57
  assert(InvitationDescriptorType.INTERACTIVE === this._invitationDescriptor.type);
61
58
  }
@@ -149,8 +146,6 @@ export class GreetingInitiator {
149
146
  const { nonce } = handshakeResponse;
150
147
  const partyKey = handshakeResponse.partyKey;
151
148
 
152
- const feedKey = await this._feedInitializer(partyKey);
153
-
154
149
  const credentialMessages = [];
155
150
  if (haloInvitation) {
156
151
  assert(this._identity.deviceKey, 'Device key required');
@@ -164,16 +159,6 @@ export class GreetingInitiator {
164
159
  [],
165
160
  nonce)
166
161
  );
167
-
168
- // And Feed, signed for by the FEED and the DEVICE.
169
- credentialMessages.push(
170
- createFeedAdmitMessage(
171
- this._identity.signer,
172
- partyKey,
173
- feedKey,
174
- [this._identity.deviceKey],
175
- nonce)
176
- );
177
162
  } else {
178
163
  assert(this._identity.deviceKeyChain, 'Device key required');
179
164
  assert(this._identity.identityGenesis, 'Identity genesis message required');
@@ -187,16 +172,6 @@ export class GreetingInitiator {
187
172
  [this._identity.deviceKeyChain],
188
173
  nonce)
189
174
  );
190
-
191
- // And the Feed, signed for by the FEED and by the DEVICE keychain, as above.
192
- credentialMessages.push(
193
- createFeedAdmitMessage(
194
- this._identity.signer,
195
- partyKey,
196
- feedKey,
197
- [this._identity.deviceKeyChain],
198
- nonce)
199
- );
200
175
  }
201
176
 
202
177
  // Send the signed payload to the greeting responder.
@@ -7,7 +7,6 @@ import debug from 'debug';
7
7
 
8
8
  import { waitForEvent } from '@dxos/async';
9
9
  import {
10
- Authenticator,
11
10
  ClaimResponse,
12
11
  Keyring,
13
12
  KeyType,
@@ -17,7 +16,8 @@ import {
17
16
  createGreetingClaimMessage,
18
17
  SecretProvider,
19
18
  SecretValidator,
20
- SignedMessage
19
+ SignedMessage,
20
+ codec
21
21
  } from '@dxos/credentials';
22
22
  import { keyToBuffer, keyToString, PublicKey, randomBytes, verify } from '@dxos/crypto';
23
23
  import { raise } from '@dxos/debug';
@@ -144,7 +144,7 @@ export class HaloRecoveryInitiator {
144
144
 
145
145
  // The secretProvider should provide an `Auth` message signed directly by the Identity key.
146
146
  createSecretProvider (): SecretProvider {
147
- return async (info: any) => Buffer.from(Authenticator.encodePayload(
147
+ return async (info: any) => Buffer.from(codec.encode(
148
148
  /* The signed portion of the Auth message includes the ID and authNonce provided
149
149
  * by "info". These values will be validated on the other end.
150
150
  */
@@ -182,7 +182,7 @@ export class HaloRecoveryInitiator {
182
182
  });
183
183
 
184
184
  const secretValidator: SecretValidator = async (invitation, secret) => {
185
- const { payload: authMessage } = Authenticator.decodePayload(secret);
185
+ const { payload: authMessage } = codec.decode(secret);
186
186
 
187
187
  return keyring.verify(<unknown>authMessage as SignedMessage) &&
188
188
  authMessage.signed.payload.partyKey.equals(invitation.id) &&
@@ -7,7 +7,6 @@ import debug from 'debug';
7
7
 
8
8
  import { waitForEvent } from '@dxos/async';
9
9
  import {
10
- Authenticator,
11
10
  ClaimResponse,
12
11
  Keyring,
13
12
  KeyType,
@@ -18,7 +17,8 @@ import {
18
17
  SecretInfo,
19
18
  SecretProvider,
20
19
  SecretValidator,
21
- SignedMessage
20
+ SignedMessage,
21
+ codec
22
22
  } from '@dxos/credentials';
23
23
  import { keyToBuffer, keyToString, PublicKey, randomBytes } from '@dxos/crypto';
24
24
  import { raise } from '@dxos/debug';
@@ -156,7 +156,7 @@ export class OfflineInvitationClaimer {
156
156
  });
157
157
 
158
158
  const secretValidator: SecretValidator = async (invitation, secret) => {
159
- const { payload: authMessage } = Authenticator.decodePayload(secret);
159
+ const { payload: authMessage } = codec.decode(secret);
160
160
 
161
161
  return keyring.verify(<unknown>authMessage as SignedMessage) &&
162
162
  authMessage.signed.payload.partyKey.equals(invitation.id) &&
@@ -172,7 +172,7 @@ export class OfflineInvitationClaimer {
172
172
  // The secretProvider should provide an `Auth` message signed directly by the Identity key.
173
173
  static createSecretProvider (identity: Identity): SecretProvider {
174
174
  return async (info?: SecretInfo) => {
175
- return Buffer.from(Authenticator.encodePayload(
175
+ return Buffer.from(codec.encode(
176
176
  /* The signed portion of the Auth message includes the ID and authNonce provided
177
177
  * by the `info` object. These values will be validated on the other end.
178
178
  */
@@ -0,0 +1,31 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import debug from 'debug';
6
+
7
+ import { Authenticator, createEnvelopeMessage, PartyAuthenticator } from '@dxos/credentials';
8
+
9
+ import { IdentityProvider } from '../halo';
10
+ import { PartyProcessor } from '../pipeline';
11
+
12
+ const log = debug('dxos:echo-db:authenticator');
13
+
14
+ export function createAuthenticator (partyProcessor: PartyProcessor, identityProvider: IdentityProvider): Authenticator {
15
+ return new PartyAuthenticator(partyProcessor.state, async auth => {
16
+ if (auth.feedAdmit && auth.feedKey && !partyProcessor.isFeedAdmitted(auth.feedKey)) {
17
+ const deviceKeyChain = identityProvider().deviceKeyChain ?? identityProvider().deviceKey;
18
+ if (!deviceKeyChain) {
19
+ log('Not device key chain available to admit new member feed');
20
+ return;
21
+ }
22
+
23
+ await partyProcessor.writeHaloMessage(createEnvelopeMessage(
24
+ identityProvider().keyring,
25
+ partyProcessor.partyKey,
26
+ auth.feedAdmit,
27
+ [deviceKeyChain]
28
+ ));
29
+ }
30
+ });
31
+ }
@@ -5,24 +5,26 @@
5
5
  import expect from 'expect';
6
6
  import { it as test } from 'mocha';
7
7
 
8
+ import { promiseTimeout } from '@dxos/async';
8
9
  import { createFeedAdmitMessage, createPartyGenesisMessage, Keyring, KeyType } from '@dxos/credentials';
9
- import { PublicKey } from '@dxos/crypto';
10
- import { codec } from '@dxos/echo-protocol';
10
+ import { createId, PublicKey } from '@dxos/crypto';
11
+ import { codec, Timeframe } from '@dxos/echo-protocol';
11
12
  import { FeedStore } from '@dxos/feed-store';
13
+ import { createTestProtocolPair } from '@dxos/mesh-protocol';
12
14
  import { ModelFactory } from '@dxos/model-factory';
13
15
  import { ObjectModel } from '@dxos/object-model';
14
- import { createStorage, STORAGE_RAM } from '@dxos/random-access-multi-storage';
16
+ import { createStorage, StorageType } from '@dxos/random-access-multi-storage';
15
17
  import { afterTest } from '@dxos/testutils';
16
18
 
17
19
  import { MetadataStore } from '../metadata';
18
- import { PartyFeedProvider } from '../pipeline';
20
+ import { PartyFeedProvider, ReplicatorProtocolPluginFactory } from '../pipeline';
19
21
  import { SnapshotStore } from '../snapshots';
20
22
  import { createRamStorage } from '../util';
21
23
  import { PartyCore } from './party-core';
22
24
 
23
25
  describe('PartyCore', () => {
24
26
  const setup = async () => {
25
- const storage = createStorage('', STORAGE_RAM);
27
+ const storage = createStorage('', StorageType.RAM);
26
28
  const feedStore = new FeedStore(storage, { valueEncoding: codec });
27
29
  afterTest(async () => feedStore.close());
28
30
 
@@ -31,7 +33,7 @@ describe('PartyCore', () => {
31
33
  const metadataStore = new MetadataStore(createRamStorage());
32
34
 
33
35
  const modelFactory = new ModelFactory().registerModel(ObjectModel);
34
- const snapshotStore = new SnapshotStore(createStorage('', STORAGE_RAM));
36
+ const snapshotStore = new SnapshotStore(createStorage('', StorageType.RAM));
35
37
 
36
38
  const partyKey = await keyring.createKeyRecord({ type: KeyType.PARTY });
37
39
 
@@ -65,10 +67,7 @@ describe('PartyCore', () => {
65
67
  [partyKey]
66
68
  ));
67
69
 
68
- // The Party key is an inception key; its SecretKey must be destroyed once the Party has been created.
69
- await keyring.deleteSecretKey(partyKey);
70
-
71
- return { party, feedKey: feed.key, feed, feedStore };
70
+ return { party, feedKey: feed.key, feed, feedStore, partyKey, keyring, partyFeedProvider };
72
71
  };
73
72
 
74
73
  test('create & have the feed key admitted', async () => {
@@ -115,4 +114,228 @@ describe('PartyCore', () => {
115
114
  expect(parent.children).toContain(child);
116
115
  }
117
116
  });
117
+
118
+ test('feed admit message triggers new feed to be opened', async () => {
119
+ const { party, partyKey, keyring, partyFeedProvider, feedStore } = await setup();
120
+
121
+ const feedKey = await keyring.createKeyRecord({ type: KeyType.FEED });
122
+
123
+ const eventFired = feedStore.feedOpenedEvent.waitForCount(1);
124
+ await party.processor.writeHaloMessage(createFeedAdmitMessage(
125
+ keyring,
126
+ party.key,
127
+ feedKey.publicKey,
128
+ [partyKey]
129
+ ));
130
+ await promiseTimeout(eventFired, 1000, new Error('timeout'));
131
+ expect(partyFeedProvider.getFeeds().find(k => k.key.equals(feedKey.publicKey))).toBeTruthy();
132
+ });
133
+
134
+ test('opens feed from hints', async () => {
135
+ const storage = createStorage('', StorageType.RAM);
136
+ const feedStore = new FeedStore(storage, { valueEncoding: codec });
137
+ afterTest(async () => feedStore.close());
138
+
139
+ const keyring = new Keyring();
140
+
141
+ const metadataStore = new MetadataStore(createRamStorage());
142
+
143
+ const modelFactory = new ModelFactory().registerModel(ObjectModel);
144
+ const snapshotStore = new SnapshotStore(createStorage('', StorageType.RAM));
145
+
146
+ const partyKey = await keyring.createKeyRecord({ type: KeyType.PARTY });
147
+
148
+ const partyFeedProvider = new PartyFeedProvider(metadataStore, keyring, feedStore, partyKey.publicKey);
149
+
150
+ const otherFeedKey = PublicKey.random();
151
+
152
+ const party = new PartyCore(
153
+ partyKey.publicKey,
154
+ partyFeedProvider,
155
+ modelFactory,
156
+ snapshotStore,
157
+ PublicKey.random()
158
+ );
159
+
160
+ await partyFeedProvider.createOrOpenWritableFeed();
161
+
162
+ const feedOpened = feedStore.feedOpenedEvent.waitForCount(1);
163
+
164
+ await party.open([{ type: KeyType.FEED, publicKey: otherFeedKey }]);
165
+ afterTest(async () => party.close());
166
+
167
+ await feedOpened;
168
+
169
+ expect(partyFeedProvider.getFeeds().some(k => k.key.equals(otherFeedKey))).toEqual(true);
170
+ });
171
+
172
+ test('manually create item', async () => {
173
+ const { party, partyFeedProvider } = await setup();
174
+ await party.open();
175
+
176
+ const feed = await partyFeedProvider.createOrOpenWritableFeed();
177
+
178
+ const itemId = createId();
179
+ await feed.feed.append({
180
+ echo: {
181
+ itemId,
182
+ genesis: {
183
+ itemType: 'dxos:example',
184
+ modelType: ObjectModel.meta.type
185
+ },
186
+ timeframe: new Timeframe()
187
+ }
188
+ });
189
+
190
+ await promiseTimeout(party.database.waitForItem({ id: itemId }), 1000, new Error('timeout'));
191
+ });
192
+
193
+ test('admit a second feed to the party', async () => {
194
+ const { party, keyring, partyKey, feedStore } = await setup();
195
+ await party.open();
196
+
197
+ const feedKey = await keyring.createKeyRecord({ type: KeyType.FEED });
198
+ const fullKey = keyring.getFullKey(feedKey.publicKey);
199
+ const feed2 = await feedStore.openReadWriteFeed(fullKey!.publicKey, fullKey!.secretKey!);
200
+
201
+ await party.processor.writeHaloMessage(createFeedAdmitMessage(
202
+ keyring,
203
+ party.key,
204
+ feed2.key,
205
+ [partyKey]
206
+ ));
207
+
208
+ const itemId = createId();
209
+ await feed2.append({
210
+ echo: {
211
+ itemId,
212
+ genesis: {
213
+ itemType: 'dxos:example',
214
+ modelType: ObjectModel.meta.type
215
+ },
216
+ timeframe: new Timeframe()
217
+ }
218
+ });
219
+
220
+ await promiseTimeout(party.database.waitForItem({ id: itemId }), 1000, new Error('timeout'));
221
+ });
222
+
223
+ test('admit feed and then open it', async () => {
224
+ const { party, keyring, partyKey, feedStore } = await setup();
225
+ await party.open();
226
+
227
+ const feedKey = await keyring.createKeyRecord({ type: KeyType.FEED });
228
+ const fullKey = keyring.getFullKey(feedKey.publicKey);
229
+
230
+ await party.processor.writeHaloMessage(createFeedAdmitMessage(
231
+ keyring,
232
+ party.key,
233
+ feedKey.publicKey,
234
+ [partyKey]
235
+ ));
236
+
237
+ const feed2 = await feedStore.openReadWriteFeed(fullKey!.publicKey, fullKey!.secretKey!);
238
+ const itemId = createId();
239
+ await feed2.append({
240
+ echo: {
241
+ itemId,
242
+ genesis: {
243
+ itemType: 'dxos:example',
244
+ modelType: ObjectModel.meta.type
245
+ },
246
+ timeframe: new Timeframe()
247
+ }
248
+ });
249
+
250
+ await promiseTimeout(party.database.waitForItem({ id: itemId }), 1000, new Error('timeout'));
251
+ });
252
+
253
+ test('self-admitting feed with a hint', async () => {
254
+ const { party, keyring, partyKey, feedStore } = await setup();
255
+ await party.open();
256
+
257
+ const feedKey = await keyring.createKeyRecord({ type: KeyType.FEED });
258
+ const fullKey = keyring.getFullKey(feedKey.publicKey);
259
+ const feed2 = await feedStore.openReadWriteFeed(fullKey!.publicKey, fullKey!.secretKey!);
260
+
261
+ await party.processor.takeHints([{
262
+ type: KeyType.FEED,
263
+ publicKey: feedKey.publicKey
264
+ }]);
265
+
266
+ await feed2.append({
267
+ halo: createFeedAdmitMessage(
268
+ keyring,
269
+ party.key,
270
+ feedKey.publicKey,
271
+ [partyKey]
272
+ )
273
+ });
274
+
275
+ const itemId = createId();
276
+ await feed2.append({
277
+ echo: {
278
+ itemId,
279
+ genesis: {
280
+ itemType: 'dxos:example',
281
+ modelType: ObjectModel.meta.type
282
+ },
283
+ timeframe: new Timeframe()
284
+ }
285
+ });
286
+
287
+ await promiseTimeout(party.database.waitForItem({ id: itemId }), 1000, new Error('timeout'));
288
+ });
289
+
290
+ test('two instances replicating', async () => {
291
+ const peer1 = await setup();
292
+
293
+ const storage = createStorage('', StorageType.RAM);
294
+ const feedStore = new FeedStore(storage, { valueEncoding: codec });
295
+ afterTest(async () => feedStore.close());
296
+
297
+ const metadataStore = new MetadataStore(createRamStorage());
298
+
299
+ const modelFactory = new ModelFactory().registerModel(ObjectModel);
300
+ const snapshotStore = new SnapshotStore(createStorage('', StorageType.RAM));
301
+
302
+ const partyFeedProvider = new PartyFeedProvider(metadataStore, peer1.keyring, feedStore, peer1.party.key);
303
+
304
+ const party2 = new PartyCore(
305
+ peer1.party.key,
306
+ partyFeedProvider,
307
+ modelFactory,
308
+ snapshotStore,
309
+ PublicKey.random()
310
+ );
311
+
312
+ const feed2 = await partyFeedProvider.createOrOpenWritableFeed();
313
+
314
+ await peer1.party.processor.writeHaloMessage(createFeedAdmitMessage(
315
+ peer1.keyring,
316
+ peer1.party.key,
317
+ feed2.key,
318
+ [peer1.partyKey]
319
+ ));
320
+
321
+ await party2.open([{
322
+ publicKey: peer1.feedKey,
323
+ type: KeyType.FEED
324
+ }]);
325
+ afterTest(async () => party2.close());
326
+
327
+ createTestProtocolPair(
328
+ new ReplicatorProtocolPluginFactory(
329
+ peer1.partyFeedProvider,
330
+ peer1.party.processor.getActiveFeedSet()
331
+ ).createPlugins().map(r => r.createExtension()),
332
+ new ReplicatorProtocolPluginFactory(
333
+ partyFeedProvider,
334
+ peer1.party.processor.getActiveFeedSet()
335
+ ).createPlugins().map(r => r.createExtension())
336
+ );
337
+
338
+ const item1 = await peer1.party.database.createItem();
339
+ await promiseTimeout(party2.database.waitForItem({ id: item1.id }), 1000, new Error('timeout'));
340
+ });
118
341
  });
@@ -113,15 +113,21 @@ export class PartyCore {
113
113
 
114
114
  if (!this._partyProcessor) {
115
115
  this._partyProcessor = new PartyProcessor(this._partyKey);
116
- if (keyHints.length > 0) {
117
- await this._partyProcessor.takeHints(keyHints);
118
- }
116
+ }
117
+ // Automatically open new admitted feeds.
118
+ this._subscriptions.push(this._partyProcessor.feedAdded.on(feed => {
119
+ void this._feedProvider.createOrOpenReadOnlyFeed(feed);
120
+ }));
121
+
122
+ if (keyHints.length > 0) {
123
+ await this._partyProcessor.takeHints(keyHints);
119
124
  }
120
125
 
121
126
  //
122
127
  // Pipeline
123
128
  //
124
129
 
130
+ await this._feedProvider.openKnownFeeds();
125
131
  const iterator = await this._feedProvider.createIterator(
126
132
  createMessageSelector(this._partyProcessor, this._timeframeClock),
127
133
  this._initialTimeframe
@@ -205,12 +205,7 @@ export class PartyFactory {
205
205
  const initiator = new GreetingInitiator(
206
206
  this._networkManager,
207
207
  identity,
208
- invitationDescriptor,
209
- async partyKey => {
210
- const feedProvider = this._createFeedProvider(partyKey);
211
- const feed = await feedProvider.createOrOpenWritableFeed();
212
- return feed.key;
213
- }
208
+ invitationDescriptor
214
209
  );
215
210
 
216
211
  await initiator.connect();
@@ -5,7 +5,7 @@
5
5
  import assert from 'assert';
6
6
 
7
7
  import { synchronized, Event } from '@dxos/async';
8
- import { KeyHint, createAuthMessage, Authenticator } from '@dxos/credentials';
8
+ import { KeyHint, createAuthMessage, createFeedAdmitMessage, codec } from '@dxos/credentials';
9
9
  import { PublicKey } from '@dxos/crypto';
10
10
  import { failUndefined, raise, timed } from '@dxos/debug';
11
11
  import { PartyKey, PartySnapshot, Timeframe, FeedKey } from '@dxos/echo-protocol';
@@ -19,6 +19,7 @@ import { ActivationOptions, PartyPreferences, IdentityProvider } from '../halo';
19
19
  import { InvitationManager } from '../invitations';
20
20
  import { CredentialsProvider, PartyFeedProvider, PartyProtocolFactory } from '../pipeline';
21
21
  import { SnapshotStore } from '../snapshots';
22
+ import { createAuthenticator } from './authenticator';
22
23
  import { PartyCore, PartyOptions } from './party-core';
23
24
  import { CONTACT_DEBOUNCE_INTERVAL } from './party-manager';
24
25
 
@@ -78,7 +79,7 @@ export class PartyInternal {
78
79
  key: this.key.toHex(),
79
80
  isOpen: this.isOpen,
80
81
  isActive: this.isActive,
81
- feedKeys: this._feedProvider.getFeedKeys().length,
82
+ feedKeys: this._feedProvider.getFeeds().length,
82
83
  timeframe: this.isOpen ? this._partyCore.timeframe : undefined,
83
84
  properties: this.isOpen ? this.getPropertiesSet().expectOne().model.toObject() : undefined
84
85
  };
@@ -178,7 +179,7 @@ export class PartyInternal {
178
179
  this._identityProvider,
179
180
  this._createCredentialsProvider(this._partyCore.key, writeFeed.key),
180
181
  this._invitationManager,
181
- this._partyCore.processor.authenticator,
182
+ createAuthenticator(this._partyCore.processor, this._identityProvider),
182
183
  this._partyCore.processor.getActiveFeedSet()
183
184
  );
184
185
 
@@ -274,12 +275,20 @@ export class PartyInternal {
274
275
  return {
275
276
  get: () => {
276
277
  const identity = this._identityProvider();
277
- return Buffer.from(Authenticator.encodePayload(createAuthMessage(
278
+ const signingKey = identity.deviceKeyChain ?? identity.deviceKey ?? raise(new IdentityNotInitializedError());
279
+ return Buffer.from(codec.encode(createAuthMessage(
278
280
  identity.signer,
279
281
  partyKey,
280
282
  identity.identityKey ?? raise(new IdentityNotInitializedError()),
281
- identity.deviceKeyChain ?? identity.deviceKey ?? raise(new IdentityNotInitializedError()),
282
- identity.keyring.getKey(feedKey)
283
+ signingKey,
284
+ identity.keyring.getKey(feedKey),
285
+ undefined,
286
+ createFeedAdmitMessage(
287
+ identity.signer,
288
+ partyKey,
289
+ feedKey,
290
+ [identity.keyring.getKey(feedKey) ?? failUndefined(), signingKey]
291
+ )
283
292
  )));
284
293
  }
285
294
  };