@dxos/client-services 0.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.
Files changed (199) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -0
  3. package/dist/src/errors.d.ts +8 -0
  4. package/dist/src/errors.d.ts.map +1 -0
  5. package/dist/src/errors.js +21 -0
  6. package/dist/src/errors.js.map +1 -0
  7. package/dist/src/index.d.ts +7 -0
  8. package/dist/src/index.d.ts.map +1 -0
  9. package/dist/src/index.js +26 -0
  10. package/dist/src/index.js.map +1 -0
  11. package/dist/src/packlets/devtools/devtools-host-events.d.ts +5 -0
  12. package/dist/src/packlets/devtools/devtools-host-events.d.ts.map +1 -0
  13. package/dist/src/packlets/devtools/devtools-host-events.js +14 -0
  14. package/dist/src/packlets/devtools/devtools-host-events.js.map +1 -0
  15. package/dist/src/packlets/devtools/feeds.d.ts +11 -0
  16. package/dist/src/packlets/devtools/feeds.d.ts.map +1 -0
  17. package/dist/src/packlets/devtools/feeds.js +72 -0
  18. package/dist/src/packlets/devtools/feeds.js.map +1 -0
  19. package/dist/src/packlets/devtools/index.d.ts +5 -0
  20. package/dist/src/packlets/devtools/index.d.ts.map +1 -0
  21. package/dist/src/packlets/devtools/index.js +24 -0
  22. package/dist/src/packlets/devtools/index.js.map +1 -0
  23. package/dist/src/packlets/devtools/network.d.ts +19 -0
  24. package/dist/src/packlets/devtools/network.d.ts.map +1 -0
  25. package/dist/src/packlets/devtools/network.js +79 -0
  26. package/dist/src/packlets/devtools/network.js.map +1 -0
  27. package/dist/src/packlets/devtools/spaces.d.ts +5 -0
  28. package/dist/src/packlets/devtools/spaces.d.ts.map +1 -0
  29. package/dist/src/packlets/devtools/spaces.js +38 -0
  30. package/dist/src/packlets/devtools/spaces.js.map +1 -0
  31. package/dist/src/packlets/identity/authenticator.d.ts +7 -0
  32. package/dist/src/packlets/identity/authenticator.d.ts.map +1 -0
  33. package/dist/src/packlets/identity/authenticator.js +40 -0
  34. package/dist/src/packlets/identity/authenticator.js.map +1 -0
  35. package/dist/src/packlets/identity/authenticator.test.d.ts +2 -0
  36. package/dist/src/packlets/identity/authenticator.test.d.ts.map +1 -0
  37. package/dist/src/packlets/identity/authenticator.test.js +32 -0
  38. package/dist/src/packlets/identity/authenticator.test.js.map +1 -0
  39. package/dist/src/packlets/identity/identity-manager.d.ts +35 -0
  40. package/dist/src/packlets/identity/identity-manager.d.ts.map +1 -0
  41. package/dist/src/packlets/identity/identity-manager.js +151 -0
  42. package/dist/src/packlets/identity/identity-manager.js.map +1 -0
  43. package/dist/src/packlets/identity/identity-manager.test.d.ts +2 -0
  44. package/dist/src/packlets/identity/identity-manager.test.d.ts.map +1 -0
  45. package/dist/src/packlets/identity/identity-manager.test.js +92 -0
  46. package/dist/src/packlets/identity/identity-manager.test.js.map +1 -0
  47. package/dist/src/packlets/identity/identity.d.ts +43 -0
  48. package/dist/src/packlets/identity/identity.d.ts.map +1 -0
  49. package/dist/src/packlets/identity/identity.js +74 -0
  50. package/dist/src/packlets/identity/identity.js.map +1 -0
  51. package/dist/src/packlets/identity/identity.test.d.ts +2 -0
  52. package/dist/src/packlets/identity/identity.test.d.ts.map +1 -0
  53. package/dist/src/packlets/identity/identity.test.js +258 -0
  54. package/dist/src/packlets/identity/identity.test.js.map +1 -0
  55. package/dist/src/packlets/identity/index.d.ts +4 -0
  56. package/dist/src/packlets/identity/index.d.ts.map +1 -0
  57. package/dist/src/packlets/identity/index.js +23 -0
  58. package/dist/src/packlets/identity/index.js.map +1 -0
  59. package/dist/src/packlets/invitations/common.d.ts +35 -0
  60. package/dist/src/packlets/invitations/common.d.ts.map +1 -0
  61. package/dist/src/packlets/invitations/common.js +12 -0
  62. package/dist/src/packlets/invitations/common.js.map +1 -0
  63. package/dist/src/packlets/invitations/halo-invitations.d.ts +23 -0
  64. package/dist/src/packlets/invitations/halo-invitations.d.ts.map +1 -0
  65. package/dist/src/packlets/invitations/halo-invitations.js +150 -0
  66. package/dist/src/packlets/invitations/halo-invitations.js.map +1 -0
  67. package/dist/src/packlets/invitations/index.d.ts +5 -0
  68. package/dist/src/packlets/invitations/index.d.ts.map +1 -0
  69. package/dist/src/packlets/invitations/index.js +24 -0
  70. package/dist/src/packlets/invitations/index.js.map +1 -0
  71. package/dist/src/packlets/invitations/invitation-wrapper.d.ts +36 -0
  72. package/dist/src/packlets/invitations/invitation-wrapper.d.ts.map +1 -0
  73. package/dist/src/packlets/invitations/invitation-wrapper.js +104 -0
  74. package/dist/src/packlets/invitations/invitation-wrapper.js.map +1 -0
  75. package/dist/src/packlets/invitations/observable.test.d.ts +2 -0
  76. package/dist/src/packlets/invitations/observable.test.d.ts.map +1 -0
  77. package/dist/src/packlets/invitations/observable.test.js +43 -0
  78. package/dist/src/packlets/invitations/observable.test.js.map +1 -0
  79. package/dist/src/packlets/invitations/space-invitations.d.ts +25 -0
  80. package/dist/src/packlets/invitations/space-invitations.d.ts.map +1 -0
  81. package/dist/src/packlets/invitations/space-invitations.js +136 -0
  82. package/dist/src/packlets/invitations/space-invitations.js.map +1 -0
  83. package/dist/src/packlets/services/impl/devtools.d.ts +34 -0
  84. package/dist/src/packlets/services/impl/devtools.d.ts.map +1 -0
  85. package/dist/src/packlets/services/impl/devtools.js +77 -0
  86. package/dist/src/packlets/services/impl/devtools.js.map +1 -0
  87. package/dist/src/packlets/services/impl/halo.d.ts +20 -0
  88. package/dist/src/packlets/services/impl/halo.d.ts.map +1 -0
  89. package/dist/src/packlets/services/impl/halo.js +93 -0
  90. package/dist/src/packlets/services/impl/halo.js.map +1 -0
  91. package/dist/src/packlets/services/impl/index.d.ts +6 -0
  92. package/dist/src/packlets/services/impl/index.d.ts.map +1 -0
  93. package/dist/src/packlets/services/impl/index.js +26 -0
  94. package/dist/src/packlets/services/impl/index.js.map +1 -0
  95. package/dist/src/packlets/services/impl/party.d.ts +25 -0
  96. package/dist/src/packlets/services/impl/party.d.ts.map +1 -0
  97. package/dist/src/packlets/services/impl/party.js +265 -0
  98. package/dist/src/packlets/services/impl/party.js.map +1 -0
  99. package/dist/src/packlets/services/impl/profile.d.ts +19 -0
  100. package/dist/src/packlets/services/impl/profile.d.ts.map +1 -0
  101. package/dist/src/packlets/services/impl/profile.js +120 -0
  102. package/dist/src/packlets/services/impl/profile.js.map +1 -0
  103. package/dist/src/packlets/services/impl/system.d.ts +9 -0
  104. package/dist/src/packlets/services/impl/system.d.ts.map +1 -0
  105. package/dist/src/packlets/services/impl/system.js +20 -0
  106. package/dist/src/packlets/services/impl/system.js.map +1 -0
  107. package/dist/src/packlets/services/impl/tracing.d.ts +12 -0
  108. package/dist/src/packlets/services/impl/tracing.d.ts.map +1 -0
  109. package/dist/src/packlets/services/impl/tracing.js +22 -0
  110. package/dist/src/packlets/services/impl/tracing.js.map +1 -0
  111. package/dist/src/packlets/services/index.d.ts +6 -0
  112. package/dist/src/packlets/services/index.d.ts.map +1 -0
  113. package/dist/src/packlets/services/index.js +25 -0
  114. package/dist/src/packlets/services/index.js.map +1 -0
  115. package/dist/src/packlets/services/service-context.d.ts +36 -0
  116. package/dist/src/packlets/services/service-context.d.ts.map +1 -0
  117. package/dist/src/packlets/services/service-context.js +112 -0
  118. package/dist/src/packlets/services/service-context.js.map +1 -0
  119. package/dist/src/packlets/services/service-factory.d.ts +15 -0
  120. package/dist/src/packlets/services/service-factory.d.ts.map +1 -0
  121. package/dist/src/packlets/services/service-factory.js +25 -0
  122. package/dist/src/packlets/services/service-factory.js.map +1 -0
  123. package/dist/src/packlets/services/service-host.d.ts +27 -0
  124. package/dist/src/packlets/services/service-host.d.ts.map +1 -0
  125. package/dist/src/packlets/services/service-host.js +78 -0
  126. package/dist/src/packlets/services/service-host.js.map +1 -0
  127. package/dist/src/packlets/services/service-host.test.d.ts +2 -0
  128. package/dist/src/packlets/services/service-host.test.d.ts.map +1 -0
  129. package/dist/src/packlets/services/service-host.test.js +78 -0
  130. package/dist/src/packlets/services/service-host.test.js.map +1 -0
  131. package/dist/src/packlets/services/services.d.ts +19 -0
  132. package/dist/src/packlets/services/services.d.ts.map +1 -0
  133. package/dist/src/packlets/services/services.js +19 -0
  134. package/dist/src/packlets/services/services.js.map +1 -0
  135. package/dist/src/packlets/services/signer.d.ts +9 -0
  136. package/dist/src/packlets/services/signer.d.ts.map +1 -0
  137. package/dist/src/packlets/services/signer.js +6 -0
  138. package/dist/src/packlets/services/signer.js.map +1 -0
  139. package/dist/src/packlets/services/testing/halo.test.d.ts +2 -0
  140. package/dist/src/packlets/services/testing/halo.test.d.ts.map +1 -0
  141. package/dist/src/packlets/services/testing/halo.test.js +34 -0
  142. package/dist/src/packlets/services/testing/halo.test.js.map +1 -0
  143. package/dist/src/packlets/services/testing/spaces.test.d.ts +2 -0
  144. package/dist/src/packlets/services/testing/spaces.test.d.ts.map +1 -0
  145. package/dist/src/packlets/services/testing/spaces.test.js +68 -0
  146. package/dist/src/packlets/services/testing/spaces.test.js.map +1 -0
  147. package/dist/src/packlets/services/testing/testing.d.ts +8 -0
  148. package/dist/src/packlets/services/testing/testing.d.ts.map +1 -0
  149. package/dist/src/packlets/services/testing/testing.js +23 -0
  150. package/dist/src/packlets/services/testing/testing.js.map +1 -0
  151. package/dist/src/packlets/storage/index.d.ts +2 -0
  152. package/dist/src/packlets/storage/index.d.ts.map +1 -0
  153. package/dist/src/packlets/storage/index.js +21 -0
  154. package/dist/src/packlets/storage/index.js.map +1 -0
  155. package/dist/src/packlets/storage/storage.d.ts +7 -0
  156. package/dist/src/packlets/storage/storage.d.ts.map +1 -0
  157. package/dist/src/packlets/storage/storage.js +89 -0
  158. package/dist/src/packlets/storage/storage.js.map +1 -0
  159. package/package.json +61 -0
  160. package/src/errors.ts +19 -0
  161. package/src/index.ts +11 -0
  162. package/src/packlets/devtools/devtools-host-events.ts +8 -0
  163. package/src/packlets/devtools/feeds.ts +91 -0
  164. package/src/packlets/devtools/index.ts +8 -0
  165. package/src/packlets/devtools/network.ts +89 -0
  166. package/src/packlets/devtools/spaces.ts +50 -0
  167. package/src/packlets/identity/authenticator.test.ts +32 -0
  168. package/src/packlets/identity/authenticator.ts +49 -0
  169. package/src/packlets/identity/identity-manager.test.ts +121 -0
  170. package/src/packlets/identity/identity-manager.ts +207 -0
  171. package/src/packlets/identity/identity.test.ts +295 -0
  172. package/src/packlets/identity/identity.ts +109 -0
  173. package/src/packlets/identity/index.ts +7 -0
  174. package/src/packlets/invitations/common.ts +48 -0
  175. package/src/packlets/invitations/halo-invitations.ts +162 -0
  176. package/src/packlets/invitations/index.ts +8 -0
  177. package/src/packlets/invitations/invitation-wrapper.ts +140 -0
  178. package/src/packlets/invitations/observable.test.ts +67 -0
  179. package/src/packlets/invitations/space-invitations.ts +159 -0
  180. package/src/packlets/services/impl/devtools.ts +141 -0
  181. package/src/packlets/services/impl/halo.ts +107 -0
  182. package/src/packlets/services/impl/index.ts +11 -0
  183. package/src/packlets/services/impl/party.ts +305 -0
  184. package/src/packlets/services/impl/profile.ts +141 -0
  185. package/src/packlets/services/impl/system.ts +19 -0
  186. package/src/packlets/services/impl/tracing.ts +21 -0
  187. package/src/packlets/services/index.ts +9 -0
  188. package/src/packlets/services/service-context.ts +151 -0
  189. package/src/packlets/services/service-factory.ts +35 -0
  190. package/src/packlets/services/service-host.test.ts +88 -0
  191. package/src/packlets/services/service-host.ts +101 -0
  192. package/src/packlets/services/services.ts +39 -0
  193. package/src/packlets/services/signer.ts +13 -0
  194. package/src/packlets/services/testing/halo.test.ts +40 -0
  195. package/src/packlets/services/testing/spaces.test.ts +87 -0
  196. package/src/packlets/services/testing/testing.ts +28 -0
  197. package/src/packlets/storage/index.ts +5 -0
  198. package/src/packlets/storage/storage.ts +97 -0
  199. package/src/typings.d.ts +5 -0
@@ -0,0 +1,109 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import assert from 'assert';
6
+
7
+ import {
8
+ DeviceStateMachine,
9
+ CredentialSigner,
10
+ createCredentialSignerWithKey,
11
+ createCredentialSignerWithChain
12
+ } from '@dxos/credentials';
13
+ import { Signer } from '@dxos/crypto';
14
+ import { failUndefined } from '@dxos/debug';
15
+ import { Database, Space } from '@dxos/echo-db';
16
+ import { PublicKey } from '@dxos/keys';
17
+ import { ComplexSet } from '@dxos/util';
18
+
19
+ export type IdentityParams = {
20
+ identityKey: PublicKey;
21
+ deviceKey: PublicKey;
22
+ signer: Signer;
23
+ space: Space;
24
+ };
25
+
26
+ /**
27
+ * Agent identity manager, which includes the agent's Halo space.
28
+ */
29
+ export class Identity {
30
+ private readonly _space: Space;
31
+ private readonly _signer: Signer;
32
+ private readonly _deviceStateMachine: DeviceStateMachine;
33
+
34
+ public readonly identityKey: PublicKey;
35
+ public readonly deviceKey: PublicKey;
36
+
37
+ constructor({ space, signer, identityKey, deviceKey }: IdentityParams) {
38
+ this._space = space;
39
+ this._signer = signer;
40
+
41
+ this.identityKey = identityKey;
42
+ this.deviceKey = deviceKey;
43
+
44
+ this._deviceStateMachine = new DeviceStateMachine(this.identityKey, this.deviceKey);
45
+
46
+ // Save device key chain credential when processed by the party state machine.
47
+ this._space.onCredentialProcessed.set(async (credential) => {
48
+ await this._deviceStateMachine.process(credential);
49
+ });
50
+ }
51
+
52
+ // TODO(burdon): Expose state object?
53
+ get authorizedDeviceKeys(): ComplexSet<PublicKey> {
54
+ return this._deviceStateMachine.authorizedDeviceKeys;
55
+ }
56
+
57
+ async open() {
58
+ await this._space.open();
59
+ }
60
+
61
+ async close() {
62
+ await this._space.close();
63
+ }
64
+
65
+ async ready() {
66
+ await this._deviceStateMachine.deviceChainReady.wait();
67
+
68
+ // TODO(dmaretskyi): Should we also wait for our feeds to be admitted?
69
+ }
70
+
71
+ /**
72
+ * @test-only
73
+ */
74
+ get controlPipeline() {
75
+ return this._space.controlPipeline;
76
+ }
77
+
78
+ get haloSpaceKey() {
79
+ return this._space.key;
80
+ }
81
+
82
+ get haloGenesisFeedKey() {
83
+ return this._space.genesisFeedKey;
84
+ }
85
+
86
+ get haloDatabase(): Database {
87
+ return this._space.database ?? failUndefined();
88
+ }
89
+
90
+ /**
91
+ * Issues credentials as identity.
92
+ * Requires identity to be ready.
93
+ */
94
+ getIdentityCredentialSigner(): CredentialSigner {
95
+ assert(this._deviceStateMachine.deviceCredentialChain, 'Device credential chain is not ready.');
96
+ return createCredentialSignerWithChain(
97
+ this._signer,
98
+ this._deviceStateMachine.deviceCredentialChain,
99
+ this.deviceKey
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Issues credentials as device.
105
+ */
106
+ getDeviceCredentialSigner(): CredentialSigner {
107
+ return createCredentialSignerWithKey(this._signer, this.deviceKey);
108
+ }
109
+ }
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export * from './authenticator';
6
+ export * from './identity';
7
+ export * from './identity-manager';
@@ -0,0 +1,48 @@
1
+ //
2
+ // Copyright 2020 DXOS.org
3
+ //
4
+
5
+ import { defaultSecretProvider, defaultSecretValidator, SecretProvider, SecretValidator } from '@dxos/credentials';
6
+
7
+ /**
8
+ * Defines a way for peers to authenticate each other through a side channel.
9
+ */
10
+ export interface InvitationAuthenticator {
11
+ secretProvider?: SecretProvider;
12
+ secretValidator: SecretValidator;
13
+ }
14
+
15
+ export const defaultInvitationAuthenticator: Required<InvitationAuthenticator> = {
16
+ secretProvider: defaultSecretProvider,
17
+ secretValidator: defaultSecretValidator
18
+ };
19
+
20
+ /**
21
+ * Additional set of callbacks and options used in the invitation process.
22
+ */
23
+ export interface InvitationOptions {
24
+ /**
25
+ * A function to be called when the invitation is closed (successfully or not).
26
+ */
27
+ onFinish?: ({ expired }: { expired?: boolean }) => void;
28
+
29
+ /**
30
+ * Date.now()-style timestamp of when this invitation should expire.
31
+ */
32
+ expiration?: number;
33
+ }
34
+
35
+ // TODO(burdon): Remove/move to API.
36
+ export type InviterInvitation = {
37
+ invitationCode: string;
38
+ secret: Uint8Array | undefined;
39
+ };
40
+
41
+ // TODO(burdon): Remove/move to API.
42
+ export type InviteeInvitation = {
43
+ secret?: Uint8Array | undefined; // Can be undefined initially, then set after receiving secret from the inviter.
44
+ secretTrigger?: () => void; // Is triggered after supplying the secret.
45
+ };
46
+
47
+ export type InviterInvitations = InviterInvitation[];
48
+ export type InviteeInvitations = Map<string, InviteeInvitation>;
@@ -0,0 +1,162 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { Trigger } from '@dxos/async';
6
+ import { failUndefined } from '@dxos/debug';
7
+ import { PublicKey } from '@dxos/keys';
8
+ import { log } from '@dxos/log';
9
+ import { createProtocolFactory, NetworkManager, StarTopology } from '@dxos/network-manager';
10
+ import { createRpcPlugin } from '@dxos/protocol-plugin-rpc';
11
+ import { schema } from '@dxos/protocols';
12
+ import { InvitationDescriptor } from '@dxos/protocols/proto/dxos/halo/invitations';
13
+ import { createProtoRpcPeer } from '@dxos/rpc';
14
+
15
+ import { Identity, IdentityManager } from '../identity';
16
+
17
+ /**
18
+ * Creates and processes Halo invitations between devices.
19
+ */
20
+ export class HaloInvitations {
21
+ constructor(
22
+ private readonly _identityManager: IdentityManager,
23
+ private readonly _networkManager: NetworkManager,
24
+ private readonly _onInitialize: () => Promise<void> // TODO(burdon): ???
25
+ ) {}
26
+
27
+ /**
28
+ * Sends a HALO admission offer and waits for guest to accept.
29
+ */
30
+ async createInvitation({ onFinish }: { onFinish?: () => void } = {}): Promise<InvitationDescriptor> {
31
+ const identity = this._identityManager.identity ?? failUndefined();
32
+
33
+ const plugin = createRpcPlugin(async (port) => {
34
+ const peer = createProtoRpcPeer({
35
+ port,
36
+ requested: {
37
+ HaloGuestService: schema.getService('dxos.halo.invitations.HaloGuestService')
38
+ },
39
+ exposed: {
40
+ HaloHostService: schema.getService('dxos.halo.invitations.HaloHostService')
41
+ },
42
+ handlers: {
43
+ HaloHostService: {
44
+ presentAdmissionCredentials: async ({ deviceKey, controlFeedKey, dataFeedKey }) => {
45
+ log('processing admission request', { identityKey: identity.identityKey, deviceKey });
46
+
47
+ // TODO(burdon): Use CredentialGenerator.
48
+ const signer = identity.getIdentityCredentialSigner();
49
+ await identity.controlPipeline.writer.write({
50
+ '@type': 'dxos.echo.feed.CredentialsMessage',
51
+ credential: await signer.createCredential({
52
+ subject: deviceKey,
53
+ assertion: {
54
+ '@type': 'dxos.halo.credentials.AuthorizedDevice',
55
+ identityKey: identity.identityKey,
56
+ deviceKey
57
+ }
58
+ })
59
+ });
60
+
61
+ // TODO(dmaretskyi): Admit guest's feeds otherwise messages from them won't be processed by the pipeline.
62
+ }
63
+ }
64
+ }
65
+ });
66
+
67
+ await peer.open();
68
+ {
69
+ log('sending admission offer', { identityKey: identity.identityKey });
70
+ await peer.rpc.HaloGuestService.presentAdmissionOffer({
71
+ identityKey: identity.identityKey,
72
+ haloSpaceKey: identity.haloSpaceKey,
73
+ genesisFeedKey: identity.haloGenesisFeedKey
74
+ });
75
+
76
+ onFinish?.();
77
+ }
78
+ await peer.close();
79
+ await connection.close();
80
+ });
81
+
82
+ const topic = PublicKey.random();
83
+ const peerId = PublicKey.random(); // TODO(burdon): Fails if using the actual peer key.
84
+ const connection = await this._networkManager.openSwarmConnection({
85
+ topic,
86
+ peerId: topic, // TODO(burdon): Why???
87
+ // peerId,
88
+ protocol: createProtocolFactory(topic, peerId, [plugin]),
89
+ topology: new StarTopology(topic)
90
+ });
91
+
92
+ return {
93
+ type: InvitationDescriptor.Type.INTERACTIVE,
94
+ swarmKey: topic.asUint8Array(),
95
+ invitation: new Uint8Array() // TODO(burdon): Remove.
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Joins an existing identity HALO by invitation.
101
+ */
102
+ async acceptInvitation(invitation: InvitationDescriptor): Promise<Identity> {
103
+ const admitted = new Trigger<Identity>(); // TODO(burdon): Must not share across multiple connections.
104
+
105
+ const plugin = createRpcPlugin(async (port) => {
106
+ const peer = createProtoRpcPeer({
107
+ port,
108
+ requested: {
109
+ // TODO(burdon): Rename in-bound.
110
+ HaloHostService: schema.getService('dxos.halo.invitations.HaloHostService')
111
+ },
112
+ exposed: {
113
+ // TODO(burdon): Rename out-bound?
114
+ HaloGuestService: schema.getService('dxos.halo.invitations.HaloGuestService')
115
+ },
116
+ handlers: {
117
+ // TODO(burdon): Factory to create handlers and open/close handlers.
118
+ HaloGuestService: {
119
+ presentAdmissionOffer: async ({ identityKey, haloSpaceKey, genesisFeedKey }) => {
120
+ log('processing admission offer', { identityKey });
121
+ const identity = await this._identityManager.acceptIdentity({
122
+ identityKey,
123
+ haloSpaceKey,
124
+ haloGenesisFeedKey: genesisFeedKey
125
+ });
126
+
127
+ log('sending admission request', { identityKey: invitation.identityKey });
128
+ await peer.rpc.HaloHostService.presentAdmissionCredentials({
129
+ deviceKey: identity.deviceKey,
130
+ controlFeedKey: PublicKey.random(),
131
+ dataFeedKey: PublicKey.random()
132
+ });
133
+
134
+ // TODO(burdon): Document.
135
+ await identity.ready();
136
+ admitted.wake(identity);
137
+ }
138
+ }
139
+ }
140
+ });
141
+
142
+ await peer.open();
143
+ await admitted.wait();
144
+ await peer.close();
145
+ });
146
+
147
+ const topic = PublicKey.from(invitation.swarmKey);
148
+ const peerId = PublicKey.random(); // TODO(burdon): Fails if using the actual peer key.
149
+ const connection = await this._networkManager.openSwarmConnection({
150
+ topic,
151
+ peerId: PublicKey.random(),
152
+ // peerId,
153
+ protocol: createProtocolFactory(topic, peerId, [plugin]),
154
+ topology: new StarTopology(topic)
155
+ });
156
+
157
+ const identity = await admitted.wait();
158
+ await this._onInitialize();
159
+ await connection.close();
160
+ return identity;
161
+ }
162
+ }
@@ -0,0 +1,8 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ export * from './common';
6
+ export * from './halo-invitations';
7
+ export * from './invitation-wrapper';
8
+ export * from './space-invitations';
@@ -0,0 +1,140 @@
1
+ //
2
+ // Copyright 2020 DXOS.org
3
+ //
4
+
5
+ import base from 'base-x';
6
+ import stableStringify from 'json-stable-stringify';
7
+ import assert from 'node:assert';
8
+
9
+ import { ripemd160 } from '@dxos/crypto';
10
+ import { InvalidInvitationError } from '@dxos/echo-db';
11
+ import { PublicKey } from '@dxos/keys';
12
+ import { InvitationDescriptor } from '@dxos/protocols/proto/dxos/halo/invitations';
13
+
14
+ // TODO(burdon): Move to Client API.
15
+
16
+ // Encode with only alpha-numeric characters.
17
+ const base62 = base('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
18
+
19
+ /**
20
+ * A serialized version of InvitationWrapper that's suitable to be encoded as an URL query string.
21
+ */
22
+ export interface InvitationQueryParameters {
23
+ hash: string;
24
+ swarmKey: string;
25
+ invitation: string;
26
+ identityKey?: string;
27
+ type: string;
28
+ }
29
+
30
+ /**
31
+ * Describes an issued invitation.
32
+ *
33
+ * Can be serialized to protobuf or JSON.
34
+ * Invitations can be interactive or offline.
35
+ *
36
+ * This descriptor might also have a bundled secret for authentication in interactive mode.
37
+ */
38
+ // TODO(burdon): Move to Client API (and/or remove).
39
+ export class InvitationWrapper {
40
+ static decode(code: string): InvitationWrapper {
41
+ const json = base62.decode(code).toString();
42
+ return InvitationWrapper.fromQueryParameters(JSON.parse(json));
43
+ }
44
+
45
+ static encode(invitation: InvitationWrapper): string {
46
+ const buffer = Buffer.from(JSON.stringify(invitation.toQueryParameters()));
47
+ return base62.encode(buffer);
48
+ }
49
+
50
+ static fromQueryParameters(queryParameters: InvitationQueryParameters): InvitationWrapper {
51
+ const { hash, swarmKey, invitation, identityKey, type } = queryParameters;
52
+ const descriptor = new InvitationWrapper(
53
+ parseInvitationType(type),
54
+ PublicKey.from(swarmKey),
55
+ PublicKey.bufferize(invitation),
56
+ identityKey ? PublicKey.from(identityKey) : undefined
57
+ );
58
+
59
+ if (hash !== descriptor.hash) {
60
+ throw new InvalidInvitationError();
61
+ }
62
+
63
+ return descriptor;
64
+ }
65
+
66
+ static fromProto(invitation: InvitationDescriptor): InvitationWrapper {
67
+ assert(invitation.type !== undefined);
68
+ assert(invitation.swarmKey, 'Missing swarm key');
69
+ assert(invitation.invitation);
70
+
71
+ return new InvitationWrapper(
72
+ invitation.type,
73
+ PublicKey.from(invitation.swarmKey),
74
+ Buffer.from(invitation.invitation),
75
+ invitation.identityKey ? PublicKey.from(invitation.identityKey) : undefined,
76
+ invitation.secret ? Buffer.from(invitation.secret) : undefined
77
+ );
78
+ }
79
+
80
+ constructor(
81
+ public readonly type: InvitationDescriptor.Type,
82
+ public readonly swarmKey: PublicKey,
83
+ public readonly invitation: Uint8Array,
84
+ public readonly identityKey?: PublicKey,
85
+ public secret?: Uint8Array
86
+ ) {
87
+ assert(type !== undefined);
88
+ assert(PublicKey.isPublicKey(swarmKey));
89
+ assert(invitation instanceof Uint8Array);
90
+ if (identityKey) {
91
+ PublicKey.assertValidPublicKey(identityKey);
92
+ }
93
+ if (secret) {
94
+ assert(secret instanceof Uint8Array);
95
+ }
96
+ }
97
+
98
+ get hash() {
99
+ const query = this.toQueryParameters();
100
+ return query.hash;
101
+ }
102
+
103
+ toProto(): InvitationDescriptor {
104
+ return {
105
+ type: this.type,
106
+ swarmKey: this.swarmKey.asUint8Array(),
107
+ invitation: this.invitation,
108
+ identityKey: this.identityKey?.asUint8Array(),
109
+ secret: this.secret
110
+ };
111
+ }
112
+
113
+ toQueryParameters(): InvitationQueryParameters {
114
+ const query: Partial<InvitationQueryParameters> = {
115
+ swarmKey: this.swarmKey.toHex(),
116
+ invitation: PublicKey.stringify(this.invitation),
117
+ type: stringifyInvitationType(this.type)
118
+ };
119
+
120
+ if (this.identityKey) {
121
+ query.identityKey = this.identityKey.toHex();
122
+ }
123
+
124
+ query.hash = ripemd160(stableStringify(query));
125
+ return query as InvitationQueryParameters;
126
+ }
127
+ }
128
+
129
+ // TODO(burdon): Move to client API.
130
+ const parseInvitationType = (str: string): InvitationDescriptor.Type => {
131
+ const type = parseInt(str);
132
+ assert(
133
+ type === InvitationDescriptor.Type.INTERACTIVE || type === InvitationDescriptor.Type.OFFLINE,
134
+ 'Invalid invitation type'
135
+ );
136
+ return type;
137
+ };
138
+
139
+ // TODO(burdon): Move to client API.
140
+ const stringifyInvitationType = (type: InvitationDescriptor.Type): string => type.toString();
@@ -0,0 +1,67 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { asyncCatch, AsyncCallbacks, Observable, ObservableImpl } from '@dxos/async';
8
+ import { Space } from '@dxos/echo-db';
9
+ import { InvitationDescriptor } from '@dxos/protocols/proto/dxos/halo/invitations';
10
+
11
+ // TODO(burdon): Timeout.
12
+ // TODO(burdon): Objective: Service impl pattern with clean open/close semantics.
13
+ // TODO(burdon): Isolate deps on protocol throughout echo-db.
14
+
15
+ import { SpaceInvitations } from './space-invitations';
16
+
17
+ interface InvitationEvents extends AsyncCallbacks {
18
+ onConnect(invitation: InvitationDescriptor): void;
19
+ onComplete(): void;
20
+ onReject(): void;
21
+ }
22
+
23
+ type InvitationObservable = Observable<InvitationEvents>;
24
+
25
+ describe('observable', function () {
26
+ it('sanity', function () {
27
+ // TODO(burdon): Testing.
28
+ const _ = async (space: Space, spaceInvitations: SpaceInvitations) => {
29
+ // Impl.
30
+ const doTest = (): InvitationObservable => {
31
+ const observable = new ObservableImpl<InvitationEvents>();
32
+
33
+ asyncCatch(
34
+ async () => {
35
+ setTimeout(() => {
36
+ observable.callbacks?.onConnect({} as InvitationDescriptor);
37
+ }, 100);
38
+ },
39
+ observable,
40
+ 30_000
41
+ );
42
+
43
+ return observable;
44
+ };
45
+
46
+ // TODO(burdon): Ability to cancel.
47
+
48
+ // Caller.
49
+ const observable = doTest();
50
+ observable.subscribe({
51
+ onConnect: (invitation) => {
52
+ console.log(invitation);
53
+ },
54
+ onComplete: () => {},
55
+ onReject: () => {},
56
+ onTimeout: (err) => {
57
+ console.log(err);
58
+ },
59
+ onError: (err: Error) => {
60
+ console.log(err);
61
+ }
62
+ });
63
+ };
64
+
65
+ expect(true).to.be.true;
66
+ });
67
+ });
@@ -0,0 +1,159 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { Trigger } from '@dxos/async';
6
+ import { createAdmissionCredentials } from '@dxos/credentials';
7
+ import { SigningContext, Space, SpaceManager } from '@dxos/echo-db';
8
+ import { writeMessages } from '@dxos/feed-store';
9
+ import { PublicKey } from '@dxos/keys';
10
+ import { log } from '@dxos/log';
11
+ import { createProtocolFactory, NetworkManager, StarTopology } from '@dxos/network-manager';
12
+ import { createRpcPlugin } from '@dxos/protocol-plugin-rpc';
13
+ import { schema } from '@dxos/protocols';
14
+ import { InvitationDescriptor } from '@dxos/protocols/proto/dxos/halo/invitations';
15
+ import { createProtoRpcPeer } from '@dxos/rpc';
16
+
17
+ /**
18
+ * Manages the life-cycle of Space invitations between peers.
19
+ */
20
+ // TODO(burdon): Rename factory.
21
+ export class SpaceInvitations {
22
+ constructor(
23
+ private readonly _spaceManager: SpaceManager,
24
+ private readonly _networkManager: NetworkManager,
25
+ private readonly _signingContext: SigningContext
26
+ ) {}
27
+
28
+ /**
29
+ * Creates an invitation and listens for a join request from the invited (guest) peer.
30
+ */
31
+ // TODO(burdon): Replace callback with cancelable observable.
32
+ async createInvitation(space: Space, { onFinish }: { onFinish?: () => void } = {}): Promise<InvitationDescriptor> {
33
+ const admitted = new Trigger();
34
+
35
+ const plugin = createRpcPlugin(async (port) => {
36
+ const peer = createProtoRpcPeer({
37
+ port,
38
+ requested: {
39
+ SpaceGuestService: schema.getService('dxos.halo.invitations.SpaceGuestService')
40
+ },
41
+ exposed: {
42
+ SpaceHostService: schema.getService('dxos.halo.invitations.SpaceHostService')
43
+ },
44
+ handlers: {
45
+ SpaceHostService: {
46
+ presentAdmissionCredentials: async ({ identityKey, deviceKey, controlFeedKey, dataFeedKey }) => {
47
+ log('processing admission request', { identityKey, deviceKey });
48
+
49
+ await writeMessages(
50
+ space.controlPipeline.writer,
51
+ await createAdmissionCredentials(
52
+ this._signingContext.credentialSigner,
53
+ identityKey,
54
+ deviceKey,
55
+ space.key,
56
+ controlFeedKey,
57
+ dataFeedKey
58
+ )
59
+ );
60
+
61
+ admitted.wake();
62
+ }
63
+ }
64
+ }
65
+ });
66
+
67
+ // TODO(burdon): Error handling and timeout.
68
+ await peer.open();
69
+ {
70
+ log('sending admission offer', { spaceKey: space.key });
71
+ await peer.rpc.SpaceGuestService.presentAdmissionOffer({
72
+ spaceKey: space.key,
73
+ genesisFeedKey: space.genesisFeedKey
74
+ });
75
+
76
+ onFinish?.();
77
+ }
78
+ await peer.close();
79
+ await connection.close();
80
+ });
81
+
82
+ const topic = PublicKey.random();
83
+ const peerId = PublicKey.random(); // TODO(burdon): Use actual key.
84
+ const connection = await this._networkManager.openSwarmConnection({
85
+ topic,
86
+ peerId: topic, // TODO(burdon): Why???
87
+ // peerId,
88
+ protocol: createProtocolFactory(topic, peerId, [plugin]),
89
+ topology: new StarTopology(topic)
90
+ });
91
+
92
+ return {
93
+ type: InvitationDescriptor.Type.INTERACTIVE, // TODO(burdon): Remove (default).
94
+ swarmKey: topic.asUint8Array(),
95
+ invitation: new Uint8Array() // TODO(burdon): Remove.
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Waits for the host peer (inviter) to accept our join request.
101
+ * The local guest peer (invitee) then sends the local party invitation to the host,
102
+ * which then writes the guest's credentials to the space.
103
+ */
104
+ async acceptInvitation(invitation: InvitationDescriptor): Promise<Space> {
105
+ const admitted = new Trigger<Space>();
106
+
107
+ const plugin = createRpcPlugin(async (port) => {
108
+ const peer = createProtoRpcPeer({
109
+ port,
110
+ requested: {
111
+ SpaceHostService: schema.getService('dxos.halo.invitations.SpaceHostService')
112
+ },
113
+ exposed: {
114
+ SpaceGuestService: schema.getService('dxos.halo.invitations.SpaceGuestService')
115
+ },
116
+ handlers: {
117
+ SpaceGuestService: {
118
+ presentAdmissionOffer: async ({ spaceKey, genesisFeedKey }) => {
119
+ log('processing admission offer', { spaceKey });
120
+ // Create local space.
121
+ const space = await this._spaceManager.acceptSpace({ spaceKey, genesisFeedKey });
122
+
123
+ // Send local space's details to host (inviter).
124
+ // TODO(burdon): Space is orphaned if we crash before other side ACKs. Retry from cold start possible?
125
+ log('sending admission request', { identityKey: invitation.identityKey });
126
+ await peer.rpc.SpaceHostService.presentAdmissionCredentials({
127
+ identityKey: this._signingContext.identityKey,
128
+ deviceKey: this._signingContext.deviceKey,
129
+ controlFeedKey: space.controlFeedKey,
130
+ dataFeedKey: space.dataFeedKey
131
+ });
132
+
133
+ // TODO(burdon): Error: Writable stream closed prematurely
134
+ admitted.wake(space);
135
+ }
136
+ }
137
+ }
138
+ });
139
+
140
+ await peer.open();
141
+ await admitted.wait();
142
+ await peer.close();
143
+ });
144
+
145
+ const topic = PublicKey.from(invitation.swarmKey);
146
+ const peerId = PublicKey.random(); // TODO(burdon): Use actual key.
147
+ const connection = await this._networkManager.openSwarmConnection({
148
+ topic,
149
+ peerId: PublicKey.random(), // TODO(burdon): Why???
150
+ // peerId,
151
+ protocol: createProtocolFactory(topic, peerId, [plugin]),
152
+ topology: new StarTopology(topic)
153
+ });
154
+
155
+ const space = await admitted.wait();
156
+ await connection.close();
157
+ return space;
158
+ }
159
+ }