@dxos/credentials 0.4.10-next.71cec10 → 0.5.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.
@@ -1 +1 @@
1
- {"version":3,"file":"space-state-machine.d.ts","sourceRoot":"","sources":["../../../../src/state-machine/space-state-machine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"space-state-machine.d.ts","sourceRoot":"","sources":["../../../../src/state-machine/space-state-machine.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/credentials",
3
- "version": "0.4.10-next.71cec10",
3
+ "version": "0.5.0",
4
4
  "description": "DXOS Credentials",
5
5
  "homepage": "https://github.com/dxos/dxos/tree/main/packages/core/halo/credentials#readme",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -25,20 +25,20 @@
25
25
  "performance-now": "^2.1.0",
26
26
  "pump": "^3.0.0",
27
27
  "stream-to-array": "^2.3.0",
28
- "@dxos/async": "0.4.10-next.71cec10",
29
- "@dxos/codec-protobuf": "0.4.10-next.71cec10",
30
- "@dxos/context": "0.4.10-next.71cec10",
31
- "@dxos/crypto": "0.4.10-next.71cec10",
32
- "@dxos/feed-store": "0.4.10-next.71cec10",
33
- "@dxos/debug": "0.4.10-next.71cec10",
34
- "@dxos/invariant": "0.4.10-next.71cec10",
35
- "@dxos/keyring": "0.4.10-next.71cec10",
36
- "@dxos/keys": "0.4.10-next.71cec10",
37
- "@dxos/node-std": "0.4.10-next.71cec10",
38
- "@dxos/timeframe": "0.4.10-next.71cec10",
39
- "@dxos/log": "0.4.10-next.71cec10",
40
- "@dxos/util": "0.4.10-next.71cec10",
41
- "@dxos/protocols": "0.4.10-next.71cec10"
28
+ "@dxos/codec-protobuf": "0.5.0",
29
+ "@dxos/context": "0.5.0",
30
+ "@dxos/crypto": "0.5.0",
31
+ "@dxos/async": "0.5.0",
32
+ "@dxos/debug": "0.5.0",
33
+ "@dxos/feed-store": "0.5.0",
34
+ "@dxos/keyring": "0.5.0",
35
+ "@dxos/invariant": "0.5.0",
36
+ "@dxos/keys": "0.5.0",
37
+ "@dxos/node-std": "0.5.0",
38
+ "@dxos/protocols": "0.5.0",
39
+ "@dxos/log": "0.5.0",
40
+ "@dxos/timeframe": "0.5.0",
41
+ "@dxos/util": "0.5.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/encoding-down": "^5.0.0",
@@ -50,7 +50,7 @@
50
50
  "@types/pify": "^3.0.2",
51
51
  "@types/pump": "^1.1.0",
52
52
  "@types/stream-to-array": "^2.3.0",
53
- "@dxos/random-access-storage": "0.4.10-next.71cec10"
53
+ "@dxos/random-access-storage": "0.5.0"
54
54
  },
55
55
  "publishConfig": {
56
56
  "access": "public"
@@ -13,6 +13,7 @@ import {
13
13
  type ProfileDocument,
14
14
  SpaceMember,
15
15
  } from '@dxos/protocols/proto/dxos/halo/credentials';
16
+ import { type DelegateSpaceInvitation } from '@dxos/protocols/proto/dxos/halo/invitations';
16
17
  import { Timeframe } from '@dxos/timeframe';
17
18
 
18
19
  import { createCredential, type CredentialSigner } from './credential-factory';
@@ -207,6 +208,7 @@ export const createAdmissionCredentials = async (
207
208
  spaceKey: PublicKey,
208
209
  genesisFeedKey: PublicKey,
209
210
  profile?: ProfileDocument,
211
+ invitationCredentialId?: PublicKey,
210
212
  ): Promise<FeedMessage.Payload[]> => {
211
213
  const credentials = await Promise.all([
212
214
  await signer.createCredential({
@@ -217,6 +219,7 @@ export const createAdmissionCredentials = async (
217
219
  role: SpaceMember.Role.ADMIN, // TODO(burdon): Configure.
218
220
  profile,
219
221
  genesisFeedKey,
222
+ invitationCredentialId,
220
223
  },
221
224
  }),
222
225
  ]);
@@ -225,3 +228,24 @@ export const createAdmissionCredentials = async (
225
228
  credential: { credential },
226
229
  }));
227
230
  };
231
+
232
+ export const createDelegatedSpaceInvitationCredential = async (
233
+ signer: CredentialSigner,
234
+ subject: PublicKey,
235
+ invitation: DelegateSpaceInvitation,
236
+ ): Promise<FeedMessage.Payload> => {
237
+ const credential = await signer.createCredential({
238
+ subject,
239
+ assertion: {
240
+ '@type': 'dxos.halo.invitations.DelegateSpaceInvitation',
241
+ invitationId: invitation.invitationId,
242
+ authMethod: invitation.authMethod,
243
+ swarmKey: invitation.swarmKey,
244
+ role: invitation.role,
245
+ guestKey: invitation.guestKey,
246
+ expiresOn: invitation.expiresOn,
247
+ multiUse: invitation.multiUse,
248
+ },
249
+ });
250
+ return { credential: { credential } };
251
+ };
@@ -5,3 +5,4 @@
5
5
  export * from './space-state-machine';
6
6
  export * from './feed-state-machine';
7
7
  export * from './member-state-machine';
8
+ export * from './invitation-state-machine';
@@ -0,0 +1,166 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { Keyring } from '@dxos/keyring';
8
+ import { PublicKey } from '@dxos/keys';
9
+ import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
10
+ import { SpaceMember, type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
11
+ import { type DelegateSpaceInvitation } from '@dxos/protocols/proto/dxos/halo/invitations';
12
+ import { describe, test } from '@dxos/test';
13
+ import { range } from '@dxos/util';
14
+
15
+ import { InvitationStateMachine } from './invitation-state-machine';
16
+ import {
17
+ createCredential,
18
+ createCredentialSignerWithKey,
19
+ createDelegatedSpaceInvitationCredential,
20
+ } from '../credentials';
21
+
22
+ describe('InvitationStateMachine', () => {
23
+ const keyring = new Keyring();
24
+ let space: PublicKey;
25
+ let identity: PublicKey;
26
+ const baseInvitation: DelegateSpaceInvitation = {
27
+ invitationId: PublicKey.random().toHex(),
28
+ swarmKey: PublicKey.random(),
29
+ role: SpaceMember.Role.ADMIN,
30
+ authMethod: Invitation.AuthMethod.KNOWN_PUBLIC_KEY,
31
+ multiUse: false,
32
+ };
33
+ beforeEach(async () => {
34
+ space = await keyring.createKey();
35
+ identity = await keyring.createKey();
36
+ });
37
+
38
+ test('invitation delegated', async () => {
39
+ const stateMachine = createStateMachine();
40
+ await stateMachine.process(await delegateInvitation(baseInvitation));
41
+ expectHasInvitation(stateMachine, baseInvitation);
42
+ });
43
+
44
+ test('multiple invitations', async () => {
45
+ const stateMachine = createStateMachine();
46
+ const invitations = range(3).map(() => ({ ...baseInvitation, invitationId: PublicKey.random().toHex() }));
47
+ await Promise.all(
48
+ invitations.map(async (invitation) => {
49
+ return stateMachine.process(await delegateInvitation(invitation));
50
+ }),
51
+ );
52
+ invitations.forEach((invitation) => expectHasInvitation(stateMachine, invitation));
53
+ });
54
+
55
+ test('expired invitations are ignored', async () => {
56
+ const stateMachine = createStateMachine();
57
+ await stateMachine.process(
58
+ await delegateInvitation({
59
+ ...baseInvitation,
60
+ expiresOn: new Date(Date.now() - 1),
61
+ }),
62
+ );
63
+ expectNoInvitation(stateMachine, baseInvitation);
64
+ });
65
+
66
+ test('single use invitation redeemed when a member joins', async () => {
67
+ const stateMachine = createStateMachine();
68
+ const delegatedInvitation = await delegateInvitation(baseInvitation);
69
+ await stateMachine.process(delegatedInvitation);
70
+ await stateMachine.process(await admitMember(delegatedInvitation));
71
+ expectNoInvitation(stateMachine, baseInvitation);
72
+ });
73
+
74
+ test('invitation stays if unrelated admission credential', async () => {
75
+ const stateMachine = createStateMachine();
76
+ const unrelatedInvitation = await delegateInvitation({
77
+ ...baseInvitation,
78
+ invitationId: PublicKey.random().toHex(),
79
+ });
80
+ await stateMachine.process(await delegateInvitation(baseInvitation));
81
+ await stateMachine.process(await admitMember(unrelatedInvitation));
82
+ expectHasInvitation(stateMachine, baseInvitation);
83
+ });
84
+
85
+ test('multi-use invitations', async () => {
86
+ const stateMachine = createStateMachine();
87
+ const multiUseInvitation = { ...baseInvitation, multiUse: true };
88
+ const delegatedInvitation = await delegateInvitation(multiUseInvitation);
89
+ await stateMachine.process(delegatedInvitation);
90
+ await stateMachine.process(await admitMember(delegatedInvitation));
91
+ await stateMachine.process(await admitMember(delegatedInvitation));
92
+ expectHasInvitation(stateMachine, multiUseInvitation);
93
+ });
94
+
95
+ test('explicit cancellation', async () => {
96
+ const stateMachine = createStateMachine();
97
+ const delegatedInvitation = await delegateInvitation(baseInvitation);
98
+ await stateMachine.process(delegatedInvitation);
99
+ await stateMachine.process(await cancelInvitation(delegatedInvitation));
100
+ expectNoInvitation(stateMachine, baseInvitation);
101
+ });
102
+
103
+ test('out of order joining', async () => {
104
+ const stateMachine = createStateMachine();
105
+ const delegatedInvitation = await delegateInvitation(baseInvitation);
106
+ await stateMachine.process(await admitMember(delegatedInvitation));
107
+ await stateMachine.process(delegatedInvitation);
108
+ expectNoInvitation(stateMachine, baseInvitation);
109
+ });
110
+
111
+ test('out of order cancellation', async () => {
112
+ const stateMachine = createStateMachine();
113
+ const delegatedInvitation = await delegateInvitation(baseInvitation);
114
+ await stateMachine.process(await cancelInvitation(delegatedInvitation));
115
+ await stateMachine.process(delegatedInvitation);
116
+ expectNoInvitation(stateMachine, baseInvitation);
117
+ });
118
+
119
+ const expectHasInvitation = (state: InvitationStateMachine, expected: DelegateSpaceInvitation) => {
120
+ const actual = [...state.invitations.values()].find((i) => i.invitationId === expected.invitationId);
121
+ expect(actual).to.deep.contain(expected);
122
+ };
123
+
124
+ const expectNoInvitation = (state: InvitationStateMachine, expected: DelegateSpaceInvitation) => {
125
+ const actual = [...state.invitations.values()].find((i) => i.invitationId === expected.invitationId);
126
+ expect(actual).to.be.undefined;
127
+ };
128
+
129
+ const delegateInvitation = async (invitation: DelegateSpaceInvitation): Promise<Credential> => {
130
+ const message = await createDelegatedSpaceInvitationCredential(
131
+ createCredentialSignerWithKey(keyring, identity),
132
+ space,
133
+ invitation,
134
+ );
135
+ return message.credential!.credential;
136
+ };
137
+
138
+ const cancelInvitation = async (invitation: Credential): Promise<Credential> => {
139
+ return createCredential({
140
+ issuer: space,
141
+ subject: identity,
142
+ assertion: {
143
+ '@type': 'dxos.halo.invitations.CancelDelegatedInvitation',
144
+ credentialId: invitation.id!,
145
+ },
146
+ signer: keyring,
147
+ });
148
+ };
149
+
150
+ const admitMember = (invitation: Credential) => {
151
+ return createCredential({
152
+ issuer: space,
153
+ subject: identity,
154
+ assertion: {
155
+ '@type': 'dxos.halo.credentials.SpaceMember',
156
+ spaceKey: space,
157
+ role: SpaceMember.Role.ADMIN,
158
+ genesisFeedKey: PublicKey.random(),
159
+ invitationCredentialId: invitation.id,
160
+ },
161
+ signer: keyring,
162
+ });
163
+ };
164
+ });
165
+
166
+ const createStateMachine = () => new InvitationStateMachine();
@@ -0,0 +1,85 @@
1
+ //
2
+ // Copyright 2022 DXOS.org
3
+ //
4
+
5
+ import { PublicKey } from '@dxos/keys';
6
+ import { type Credential } from '@dxos/protocols/proto/dxos/halo/credentials';
7
+ import { type DelegateSpaceInvitation } from '@dxos/protocols/proto/dxos/halo/invitations';
8
+ import { type AsyncCallback, Callback, ComplexMap, ComplexSet } from '@dxos/util';
9
+
10
+ import { getCredentialAssertion } from '../credentials';
11
+
12
+ export interface DelegateInvitationCredential {
13
+ credentialId: PublicKey;
14
+ invitation: DelegateSpaceInvitation;
15
+ }
16
+
17
+ /**
18
+ * Tracks the feed tree for a space.
19
+ * Provides a list of admitted feeds.
20
+ */
21
+ export class InvitationStateMachine {
22
+ private readonly _invitations = new ComplexMap<PublicKey, DelegateSpaceInvitation>(PublicKey.hash);
23
+ private readonly _redeemedInvitationCredentialIds = new ComplexSet(PublicKey.hash);
24
+ private readonly _cancelledInvitationCredentialIds = new ComplexSet(PublicKey.hash);
25
+
26
+ readonly onDelegatedInvitation = new Callback<AsyncCallback<DelegateInvitationCredential>>();
27
+ readonly onDelegatedInvitationRemoved = new Callback<AsyncCallback<DelegateInvitationCredential>>();
28
+
29
+ get invitations(): ReadonlyMap<PublicKey, DelegateSpaceInvitation> {
30
+ return this._invitations;
31
+ }
32
+
33
+ async process(credential: Credential): Promise<void> {
34
+ const credentialId = credential.id;
35
+ if (credentialId == null) {
36
+ return;
37
+ }
38
+ const assertion = getCredentialAssertion(credential);
39
+ switch (assertion['@type']) {
40
+ case 'dxos.halo.invitations.CancelDelegatedInvitation': {
41
+ this._cancelledInvitationCredentialIds.add(assertion.credentialId);
42
+ const existingInvitation = this._invitations.get(assertion.credentialId);
43
+ if (existingInvitation != null) {
44
+ this._invitations.delete(assertion.credentialId);
45
+ await this.onDelegatedInvitationRemoved.callIfSet({
46
+ credentialId: assertion.credentialId,
47
+ invitation: existingInvitation,
48
+ });
49
+ }
50
+ break;
51
+ }
52
+ case 'dxos.halo.invitations.DelegateSpaceInvitation': {
53
+ if (credential.id) {
54
+ const isExpired = assertion.expiresOn && assertion.expiresOn.getTime() < Date.now();
55
+ const wasUsed = this._redeemedInvitationCredentialIds.has(credential.id) && !assertion.multiUse;
56
+ const wasCancelled = this._cancelledInvitationCredentialIds.has(credential.id);
57
+ if (isExpired || wasCancelled || wasUsed) {
58
+ return;
59
+ }
60
+ const invitation: DelegateSpaceInvitation = { ...assertion };
61
+ this._invitations.set(credential.id, invitation);
62
+ await this.onDelegatedInvitation.callIfSet({
63
+ credentialId: credential.id,
64
+ invitation,
65
+ });
66
+ }
67
+ break;
68
+ }
69
+ case 'dxos.halo.credentials.SpaceMember': {
70
+ if (assertion.invitationCredentialId != null) {
71
+ this._redeemedInvitationCredentialIds.add(assertion.invitationCredentialId);
72
+ const existingInvitation = this._invitations.get(assertion.invitationCredentialId);
73
+ if (existingInvitation != null && !existingInvitation.multiUse) {
74
+ this._invitations.delete(assertion.invitationCredentialId);
75
+ await this.onDelegatedInvitationRemoved.callIfSet({
76
+ credentialId: assertion.invitationCredentialId,
77
+ invitation: existingInvitation,
78
+ });
79
+ }
80
+ }
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ }
@@ -8,9 +8,11 @@ import { PublicKey } from '@dxos/keys';
8
8
  import { log } from '@dxos/log';
9
9
  import { type TypedMessage } from '@dxos/protocols';
10
10
  import { type Credential, SpaceMember } from '@dxos/protocols/proto/dxos/halo/credentials';
11
+ import { type DelegateSpaceInvitation } from '@dxos/protocols/proto/dxos/halo/invitations';
11
12
  import { type AsyncCallback, Callback, ComplexMap, ComplexSet } from '@dxos/util';
12
13
 
13
14
  import { type FeedInfo, FeedStateMachine } from './feed-state-machine';
15
+ import { InvitationStateMachine } from './invitation-state-machine';
14
16
  import { MemberStateMachine, type MemberInfo } from './member-state-machine';
15
17
  import { getCredentialAssertion, verifyCredential } from '../credentials';
16
18
  import { type CredentialProcessor } from '../processor/credential-processor';
@@ -21,6 +23,7 @@ export interface SpaceState {
21
23
  readonly credentials: Credential[];
22
24
  readonly genesisCredential: Credential | undefined;
23
25
  readonly creator: MemberInfo | undefined;
26
+ readonly invitations: ReadonlyMap<PublicKey, DelegateSpaceInvitation>;
24
27
 
25
28
  addCredentialProcessor(processor: CredentialProcessor): Promise<void>;
26
29
  removeCredentialProcessor(processor: CredentialProcessor): Promise<void>;
@@ -39,7 +42,7 @@ export type CredentialEntry = {
39
42
  revoked: boolean;
40
43
  };
41
44
 
42
- const REVOKABLE_CREDENTIALS = ['dxos.halo.credentials.SpaceMember'];
45
+ const REVOCABLE_CREDENTIALS = ['dxos.halo.credentials.SpaceMember'];
43
46
 
44
47
  /**
45
48
  * Validates and processes credentials for a single space.
@@ -49,6 +52,7 @@ const REVOKABLE_CREDENTIALS = ['dxos.halo.credentials.SpaceMember'];
49
52
  export class SpaceStateMachine implements SpaceState {
50
53
  private readonly _members = new MemberStateMachine(this._spaceKey);
51
54
  private readonly _feeds = new FeedStateMachine(this._spaceKey);
55
+ private readonly _invitations = new InvitationStateMachine();
52
56
  private readonly _credentials: CredentialEntry[] = [];
53
57
  private readonly _credentialsById = new ComplexMap<PublicKey, CredentialEntry>(PublicKey.hash);
54
58
  private readonly _processedCredentials = new ComplexSet<PublicKey>(PublicKey.hash);
@@ -59,6 +63,8 @@ export class SpaceStateMachine implements SpaceState {
59
63
  readonly onCredentialProcessed = new Callback<AsyncCallback<Credential>>();
60
64
  readonly onMemberAdmitted = this._members.onMemberAdmitted;
61
65
  readonly onFeedAdmitted = this._feeds.onFeedAdmitted;
66
+ readonly onDelegatedInvitation = this._invitations.onDelegatedInvitation;
67
+ readonly onDelegatedInvitationRemoved = this._invitations.onDelegatedInvitationRemoved;
62
68
 
63
69
  constructor(private readonly _spaceKey: PublicKey) {}
64
70
 
@@ -86,6 +92,10 @@ export class SpaceStateMachine implements SpaceState {
86
92
  return this._genesisCredential;
87
93
  }
88
94
 
95
+ get invitations(): ReadonlyMap<PublicKey, DelegateSpaceInvitation> {
96
+ return this._invitations.invitations;
97
+ }
98
+
89
99
  async addCredentialProcessor(processor: CredentialProcessor) {
90
100
  if (this._credentialProcessors.find((p) => p.processor === processor)) {
91
101
  throw new Error('Credential processor already added.');
@@ -159,7 +169,6 @@ export class SpaceStateMachine implements SpaceState {
159
169
 
160
170
  toBeRevoked.revoked = true;
161
171
  await this._members.onRevoked(toBeRevoked.credential, credential);
162
-
163
172
  break;
164
173
  }
165
174
  case 'dxos.halo.credentials.SpaceGenesis': {
@@ -195,6 +204,7 @@ export class SpaceStateMachine implements SpaceState {
195
204
  }
196
205
 
197
206
  await this._members.process(credential);
207
+ await this._invitations.process(credential);
198
208
  break;
199
209
  }
200
210
 
@@ -222,6 +232,15 @@ export class SpaceStateMachine implements SpaceState {
222
232
  await this._feeds.process(credential, sourceFeed);
223
233
  break;
224
234
  }
235
+ case 'dxos.halo.invitations.CancelDelegatedInvitation':
236
+ case 'dxos.halo.invitations.DelegateSpaceInvitation': {
237
+ if (!this._canInviteNewMembers(credential.issuer)) {
238
+ log.warn(`Invalid invitation, space member is not authorized to invite new members: ${credential.issuer}`);
239
+ return false;
240
+ }
241
+ await this._invitations.process(credential);
242
+ break;
243
+ }
225
244
  }
226
245
 
227
246
  const newEntry: CredentialEntry = { credential, sourceFeed, revoked: false };
@@ -257,7 +276,7 @@ export class SpaceStateMachine implements SpaceState {
257
276
 
258
277
  private _credentialCanBeRevoked(entry: CredentialEntry) {
259
278
  // TODO(dmaretskyi): Prohibit from removing space's creator member credential.
260
- return REVOKABLE_CREDENTIALS.includes(entry.credential.subject.assertion['@type']);
279
+ return REVOCABLE_CREDENTIALS.includes(entry.credential.subject.assertion['@type']);
261
280
  }
262
281
  }
263
282