@dxos/credentials 0.8.4-main.ae835ea → 0.8.4-main.bc2380dfbc

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 (51) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/{browser → neutral}/index.mjs +75 -311
  4. package/dist/lib/neutral/index.mjs.map +7 -0
  5. package/dist/lib/neutral/meta.json +1 -0
  6. package/dist/types/src/credentials/assertions.d.ts.map +1 -1
  7. package/dist/types/src/credentials/credential-factory.d.ts +4 -4
  8. package/dist/types/src/credentials/credential-factory.d.ts.map +1 -1
  9. package/dist/types/src/credentials/credential-generator.d.ts +3 -3
  10. package/dist/types/src/credentials/credential-generator.d.ts.map +1 -1
  11. package/dist/types/src/credentials/signing.d.ts.map +1 -1
  12. package/dist/types/src/credentials/verifier.d.ts.map +1 -1
  13. package/dist/types/src/did.d.ts.map +1 -1
  14. package/dist/types/src/graph/credential-graph.d.ts.map +1 -1
  15. package/dist/types/src/passcode.d.ts.map +1 -1
  16. package/dist/types/src/presentations/presentation.d.ts.map +1 -1
  17. package/dist/types/src/presentations/signing.d.ts.map +1 -1
  18. package/dist/types/src/presentations/verifier.d.ts.map +1 -1
  19. package/dist/types/src/processor/device-state-machine.d.ts +2 -2
  20. package/dist/types/src/processor/device-state-machine.d.ts.map +1 -1
  21. package/dist/types/src/processor/profile-state-machine.d.ts +2 -2
  22. package/dist/types/src/processor/profile-state-machine.d.ts.map +1 -1
  23. package/dist/types/src/seedphrase.d.ts.map +1 -1
  24. package/dist/types/src/state-machine/feed-state-machine.d.ts.map +1 -1
  25. package/dist/types/src/state-machine/invitation-state-machine.d.ts.map +1 -1
  26. package/dist/types/src/state-machine/member-state-machine.d.ts.map +1 -1
  27. package/dist/types/src/state-machine/space-state-machine.d.ts +7 -1
  28. package/dist/types/src/state-machine/space-state-machine.d.ts.map +1 -1
  29. package/dist/types/tsconfig.tsbuildinfo +1 -1
  30. package/docs/README.md +17 -17
  31. package/moon.yml +2 -1
  32. package/package.json +19 -20
  33. package/src/credentials/credential-factory.ts +4 -4
  34. package/src/credentials/credential-generator.ts +5 -0
  35. package/src/credentials/signing.ts +12 -0
  36. package/src/presentations/json-encoding.test.ts +0 -2
  37. package/src/presentations/presentation.ts +0 -1
  38. package/src/presentations/verifier.test.ts +0 -1
  39. package/src/presentations/verifier.ts +0 -1
  40. package/src/processor/device-state-machine.ts +2 -3
  41. package/src/processor/profile-state-machine.ts +2 -3
  42. package/src/state-machine/invitation-state-machine.test.ts +0 -1
  43. package/src/state-machine/member-state-machine.test.ts +0 -1
  44. package/src/state-machine/space-state-machine.test.ts +243 -2
  45. package/src/state-machine/space-state-machine.ts +20 -2
  46. package/.swc/plugins/linux_x86_64_19.0.0/727453fb3a62f7f1d952a41e051ca8a6f88cadc45cee43c6a4d1aa45f9b75665.wasmer-v7 +0 -0
  47. package/dist/lib/browser/index.mjs.map +0 -7
  48. package/dist/lib/browser/meta.json +0 -1
  49. package/dist/lib/node-esm/index.mjs +0 -1912
  50. package/dist/lib/node-esm/index.mjs.map +0 -7
  51. package/dist/lib/node-esm/meta.json +0 -1
package/docs/README.md CHANGED
@@ -84,20 +84,20 @@ dxos/mesh-protocol --> dxos/codec-protobuf
84
84
 
85
85
  ## Dependencies
86
86
 
87
- | Module | Direct |
88
- |---|---|
89
- | [`@dxos/async`](../../../../common/async/docs/README.md) | ✓ |
90
- | [`@dxos/codec-protobuf`](../../../../common/codec-protobuf/docs/README.md) | ✓ |
91
- | [`@dxos/context`](../../../../common/context/docs/README.md) | |
92
- | [`@dxos/crypto`](../../../../common/crypto/docs/README.md) | ✓ |
93
- | [`@dxos/debug`](../../../../common/debug/docs/README.md) | ✓ |
94
- | [`@dxos/feed-store`](../../../../common/feed-store/docs/README.md) | ✓ |
95
- | [`@dxos/hypercore`](../../../../common/hypercore/docs/README.md) | |
96
- | [`@dxos/keyring`](../../keyring/docs/README.md) | ✓ |
97
- | [`@dxos/keys`](../../../../common/keys/docs/README.md) | ✓ |
98
- | [`@dxos/log`](../../../../common/log/docs/README.md) | ✓ |
99
- | [`@dxos/mesh-protocol`](../../../mesh/mesh-protocol/docs/README.md) | ✓ |
100
- | [`@dxos/protocols`](../../../protocols/docs/README.md) | ✓ |
101
- | [`@dxos/random-access-storage`](../../../../common/random-access-storage/docs/README.md) | |
102
- | [`@dxos/timeframe`](../../../../common/timeframe/docs/README.md) | |
103
- | [`@dxos/util`](../../../../common/util/docs/README.md) | ✓ |
87
+ | Module | Direct |
88
+ | ---------------------------------------------------------------------------------------- | ------- |
89
+ | [`@dxos/async`](../../../../common/async/docs/README.md) | ✓ |
90
+ | [`@dxos/codec-protobuf`](../../../../common/codec-protobuf/docs/README.md) | ✓ |
91
+ | [`@dxos/context`](../../../../common/context/docs/README.md) | |
92
+ | [`@dxos/crypto`](../../../../common/crypto/docs/README.md) | ✓ |
93
+ | [`@dxos/debug`](../../../../common/debug/docs/README.md) | ✓ |
94
+ | [`@dxos/feed-store`](../../../../common/feed-store/docs/README.md) | ✓ |
95
+ | [`@dxos/hypercore`](../../../../common/hypercore/docs/README.md) | |
96
+ | [`@dxos/keyring`](../../keyring/docs/README.md) | ✓ |
97
+ | [`@dxos/keys`](../../../../common/keys/docs/README.md) | ✓ |
98
+ | [`@dxos/log`](../../../../common/log/docs/README.md) | ✓ |
99
+ | [`@dxos/mesh-protocol`](../../../mesh/mesh-protocol/docs/README.md) | ✓ |
100
+ | [`@dxos/protocols`](../../../protocols/docs/README.md) | ✓ |
101
+ | [`@dxos/random-access-storage`](../../../../common/random-access-storage/docs/README.md) | |
102
+ | [`@dxos/timeframe`](../../../../common/timeframe/docs/README.md) | |
103
+ | [`@dxos/util`](../../../../common/util/docs/README.md) | ✓ |
package/moon.yml CHANGED
@@ -1,4 +1,4 @@
1
- type: library
1
+ layer: library
2
2
  language: typescript
3
3
  tags:
4
4
  - ts-build
@@ -9,3 +9,4 @@ tasks:
9
9
  args:
10
10
  - '--entryPoint=src/index.ts'
11
11
  - '--injectGlobals'
12
+ - '--platform=neutral'
package/package.json CHANGED
@@ -1,40 +1,39 @@
1
1
  {
2
2
  "name": "@dxos/credentials",
3
- "version": "0.8.4-main.ae835ea",
3
+ "version": "0.8.4-main.bc2380dfbc",
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",
7
- "repository": "github:dxos/dxos",
8
- "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
11
+ "license": "FSL-1.1-Apache-2.0",
9
12
  "author": "info@dxos.org",
10
- "sideEffects": true,
13
+ "sideEffects": false,
11
14
  "type": "module",
12
15
  "exports": {
13
16
  ".": {
14
17
  "source": "./src/index.ts",
15
18
  "types": "./dist/types/src/index.d.ts",
16
- "browser": "./dist/lib/browser/index.mjs",
17
- "node": "./dist/lib/node-esm/index.mjs"
19
+ "default": "./dist/lib/neutral/index.mjs"
18
20
  }
19
21
  },
20
22
  "types": "dist/types/src/index.d.ts",
21
- "typesVersions": {
22
- "*": {}
23
- },
24
23
  "dependencies": {
25
24
  "bip39": "^3.1.0",
26
25
  "json-stable-stringify": "^1.3.0",
27
- "@dxos/async": "0.8.4-main.ae835ea",
28
- "@dxos/context": "0.8.4-main.ae835ea",
29
- "@dxos/crypto": "0.8.4-main.ae835ea",
30
- "@dxos/invariant": "0.8.4-main.ae835ea",
31
- "@dxos/keys": "0.8.4-main.ae835ea",
32
- "@dxos/log": "0.8.4-main.ae835ea",
33
- "@dxos/keyring": "0.8.4-main.ae835ea",
34
- "@dxos/node-std": "0.8.4-main.ae835ea",
35
- "@dxos/protocols": "0.8.4-main.ae835ea",
36
- "@dxos/timeframe": "0.8.4-main.ae835ea",
37
- "@dxos/util": "0.8.4-main.ae835ea"
26
+ "@dxos/async": "0.8.4-main.bc2380dfbc",
27
+ "@dxos/crypto": "0.8.4-main.bc2380dfbc",
28
+ "@dxos/context": "0.8.4-main.bc2380dfbc",
29
+ "@dxos/keyring": "0.8.4-main.bc2380dfbc",
30
+ "@dxos/invariant": "0.8.4-main.bc2380dfbc",
31
+ "@dxos/keys": "0.8.4-main.bc2380dfbc",
32
+ "@dxos/log": "0.8.4-main.bc2380dfbc",
33
+ "@dxos/node-std": "0.8.4-main.bc2380dfbc",
34
+ "@dxos/timeframe": "0.8.4-main.bc2380dfbc",
35
+ "@dxos/util": "0.8.4-main.bc2380dfbc",
36
+ "@dxos/protocols": "0.8.4-main.bc2380dfbc"
38
37
  },
39
38
  "publishConfig": {
40
39
  "access": "public"
@@ -11,14 +11,14 @@ import { type Chain, type Credential } from '@dxos/protocols/proto/dxos/halo/cre
11
11
  import { getCredentialProofPayload } from './signing';
12
12
  import { SIGNATURE_TYPE_ED25519, verifyChain } from './verifier';
13
13
 
14
- export type CreateCredentialSignerParams = {
14
+ export type CreateCredentialSignerProps = {
15
15
  subject: PublicKey;
16
16
  assertion: TypedMessage;
17
17
  nonce?: Uint8Array;
18
18
  parentCredentialIds?: PublicKey[];
19
19
  };
20
20
 
21
- export type CreateCredentialParams = {
21
+ export type CreateCredentialProps = {
22
22
  signer: Signer;
23
23
  issuer: PublicKey;
24
24
  signingKey?: PublicKey;
@@ -44,7 +44,7 @@ export const createCredential = async ({
44
44
  chain,
45
45
  nonce,
46
46
  parentCredentialIds,
47
- }: CreateCredentialParams): Promise<Credential> => {
47
+ }: CreateCredentialProps): Promise<Credential> => {
48
48
  invariant(assertion['@type'], 'Invalid assertion.');
49
49
  invariant(!!signingKey === !!chain, 'Chain must be provided if and only if the signing key differs from the issuer.');
50
50
  if (chain) {
@@ -93,7 +93,7 @@ export const createCredentialMessage = (credential: Credential) => {
93
93
  // TODO(burdon): Vs. Signer.
94
94
  export interface CredentialSigner {
95
95
  getIssuer(): PublicKey;
96
- createCredential: (params: CreateCredentialSignerParams) => Promise<Credential>;
96
+ createCredential: (params: CreateCredentialSignerProps) => Promise<Credential>;
97
97
  }
98
98
 
99
99
  /**
@@ -10,6 +10,7 @@ import {
10
10
  AdmittedFeed,
11
11
  type Credential,
12
12
  type DeviceProfileDocument,
13
+ MembershipPolicy,
13
14
  type ProfileDocument,
14
15
  SpaceMember,
15
16
  } from '@dxos/protocols/proto/dxos/halo/credentials';
@@ -38,6 +39,7 @@ export class CredentialGenerator {
38
39
  spaceKey: PublicKey,
39
40
  controlKey: PublicKey,
40
41
  creatorProfile?: ProfileDocument,
42
+ membershipPolicy: MembershipPolicy = MembershipPolicy.INVITE,
41
43
  ): Promise<Credential[]> {
42
44
  return [
43
45
  await createCredential({
@@ -47,6 +49,7 @@ export class CredentialGenerator {
47
49
  assertion: {
48
50
  '@type': 'dxos.halo.credentials.SpaceGenesis',
49
51
  spaceKey,
52
+ membershipPolicy,
50
53
  },
51
54
  }),
52
55
 
@@ -221,6 +224,7 @@ export const createAdmissionCredentials = async (
221
224
  membershipChainHeads: PublicKey[] = [],
222
225
  profile?: ProfileDocument,
223
226
  invitationCredentialId?: PublicKey,
227
+ tags?: string[],
224
228
  ): Promise<FeedMessage.Payload[]> => {
225
229
  const credentials = await Promise.all([
226
230
  await signer.createCredential({
@@ -233,6 +237,7 @@ export const createAdmissionCredentials = async (
233
237
  profile,
234
238
  genesisFeedKey,
235
239
  invitationCredentialId,
240
+ tags: tags ?? [],
236
241
  },
237
242
  }),
238
243
  ]);
@@ -27,6 +27,18 @@ export const getCredentialProofPayload = (credential: Credential): Uint8Array =>
27
27
  }
28
28
  delete copy.id; // ID is not part of the signature payload.
29
29
 
30
+ // Normalize empty repeated fields in the assertion to avoid proto3 serialization asymmetry.
31
+ // Proto3 does not encode empty repeated fields, but the codec may restore them as [] after deserialization,
32
+ // causing a signature mismatch if the original signing payload did not include the field.
33
+ const assertion = (copy.subject as any)?.assertion;
34
+ if (assertion) {
35
+ for (const key of Object.keys(assertion)) {
36
+ if (Array.isArray(assertion[key]) && assertion[key].length === 0) {
37
+ delete assertion[key];
38
+ }
39
+ }
40
+ }
41
+
30
42
  return Buffer.from(canonicalStringify(copy));
31
43
  };
32
44
 
@@ -3,7 +3,6 @@
3
3
  //
4
4
 
5
5
  import { inspect } from 'util';
6
-
7
6
  import { describe, test } from 'vitest';
8
7
 
9
8
  import { randomBytes } from '@dxos/crypto';
@@ -13,7 +12,6 @@ import { PresentationSchema } from '@dxos/protocols/buf/dxos/halo/credentials_pb
13
12
  import { schema } from '@dxos/protocols/proto';
14
13
 
15
14
  import { createCredential } from '../credentials';
16
-
17
15
  import { signPresentation } from './presentation';
18
16
 
19
17
  describe('json encoding', () => {
@@ -7,7 +7,6 @@ import { type PublicKey } from '@dxos/keys';
7
7
  import { type Chain, type Presentation, type Proof } from '@dxos/protocols/proto/dxos/halo/credentials';
8
8
 
9
9
  import { SIGNATURE_TYPE_ED25519 } from '../credentials';
10
-
11
10
  import { getPresentationProofPayload } from './signing';
12
11
 
13
12
  // TODO(burdon): Rename createPresentation?
@@ -10,7 +10,6 @@ import { PublicKey } from '@dxos/keys';
10
10
  import { type Chain, SpaceMember } from '@dxos/protocols/proto/dxos/halo/credentials';
11
11
 
12
12
  import { createCredential } from '../credentials';
13
-
14
13
  import { signPresentation } from './presentation';
15
14
  import { verifyPresentation, verifyPresentationSignature } from './verifier';
16
15
 
@@ -6,7 +6,6 @@ import { verifySignature } from '@dxos/crypto';
6
6
  import { type Presentation, type Proof } from '@dxos/protocols/proto/dxos/halo/credentials';
7
7
 
8
8
  import { SIGNATURE_TYPE_ED25519, type VerificationResult, verifyChain, verifyCredential } from '../credentials';
9
-
10
9
  import { getPresentationProofPayload } from './signing';
11
10
 
12
11
  export const verifyPresentation = async (presentation: Presentation): Promise<VerificationResult> => {
@@ -10,10 +10,9 @@ import { type Chain, type Credential, type DeviceProfileDocument } from '@dxos/p
10
10
  import { ComplexMap } from '@dxos/util';
11
11
 
12
12
  import { getCredentialAssertion, isValidAuthorizedDeviceCredential } from '../credentials';
13
-
14
13
  import { type CredentialProcessor } from './credential-processor';
15
14
 
16
- export type DeviceStateMachineParams = {
15
+ export type DeviceStateMachineProps = {
17
16
  identityKey: PublicKey;
18
17
  deviceKey: PublicKey;
19
18
  onUpdate?: () => void;
@@ -30,7 +29,7 @@ export class DeviceStateMachine implements CredentialProcessor {
30
29
 
31
30
  public deviceCredentialChain?: Chain;
32
31
 
33
- constructor(private readonly _params: DeviceStateMachineParams) {}
32
+ constructor(private readonly _params: DeviceStateMachineProps) {}
34
33
 
35
34
  async processCredential(credential: Credential): Promise<void> {
36
35
  log('processing credential...', {
@@ -7,10 +7,9 @@ import { log } from '@dxos/log';
7
7
  import { type Credential, type ProfileDocument } from '@dxos/protocols/proto/dxos/halo/credentials';
8
8
 
9
9
  import { getCredentialAssertion } from '../credentials';
10
-
11
10
  import { type CredentialProcessor } from './credential-processor';
12
11
 
13
- export type ProfileStateMachineParams = {
12
+ export type ProfileStateMachineProps = {
14
13
  identityKey: PublicKey;
15
14
  onUpdate?: () => void;
16
15
  };
@@ -22,7 +21,7 @@ export class ProfileStateMachine implements CredentialProcessor {
22
21
  // TODO(burdon): Return values via getter.
23
22
  public profile?: ProfileDocument;
24
23
 
25
- constructor(private readonly _params: ProfileStateMachineParams) {}
24
+ constructor(private readonly _params: ProfileStateMachineProps) {}
26
25
 
27
26
  async processCredential(credential: Credential): Promise<void> {
28
27
  const assertion = getCredentialAssertion(credential);
@@ -16,7 +16,6 @@ import {
16
16
  createCredentialSignerWithKey,
17
17
  createDelegatedSpaceInvitationCredential,
18
18
  } from '../credentials';
19
-
20
19
  import { InvitationStateMachine } from './invitation-state-machine';
21
20
 
22
21
  describe('InvitationStateMachine', () => {
@@ -10,7 +10,6 @@ import { SpaceMember } from '@dxos/protocols/proto/dxos/halo/credentials';
10
10
  import { range } from '@dxos/util';
11
11
 
12
12
  import { createAdmissionCredentials, createCredentialSignerWithKey } from '../credentials';
13
-
14
13
  import { MemberStateMachine } from './member-state-machine';
15
14
 
16
15
  const keyring = new Keyring();
@@ -6,10 +6,9 @@ import { describe, expect, test } from 'vitest';
6
6
 
7
7
  import { Keyring } from '@dxos/keyring';
8
8
  import { type PublicKey } from '@dxos/keys';
9
- import { AdmittedFeed, type Chain, SpaceMember } from '@dxos/protocols/proto/dxos/halo/credentials';
9
+ import { AdmittedFeed, type Chain, MembershipPolicy, SpaceMember } from '@dxos/protocols/proto/dxos/halo/credentials';
10
10
 
11
11
  import { createCredential, verifyCredential } from '../credentials';
12
-
13
12
  import { SpaceStateMachine } from './space-state-machine';
14
13
 
15
14
  describe('SpaceStateMachine', () => {
@@ -30,6 +29,7 @@ describe('SpaceStateMachine', () => {
30
29
  assertion: {
31
30
  '@type': 'dxos.halo.credentials.SpaceGenesis',
32
31
  spaceKey: space,
32
+ membershipPolicy: MembershipPolicy.INVITE,
33
33
  },
34
34
  signer: keyring,
35
35
  }),
@@ -130,6 +130,7 @@ describe('SpaceStateMachine', () => {
130
130
  assertion: {
131
131
  '@type': 'dxos.halo.credentials.SpaceGenesis',
132
132
  spaceKey: space,
133
+ membershipPolicy: MembershipPolicy.INVITE,
133
134
  },
134
135
  signer: keyring,
135
136
  }),
@@ -211,6 +212,60 @@ describe('SpaceStateMachine', () => {
211
212
  expect(spaceState.credentials).toHaveLength(3);
212
213
  });
213
214
 
215
+ test('space genesis with tags', async () => {
216
+ const keyring = new Keyring();
217
+ const space = await keyring.createKey();
218
+ const identity = await keyring.createKey();
219
+ const feed = await keyring.createKey();
220
+
221
+ const spaceState = new SpaceStateMachine(space);
222
+
223
+ expect(spaceState.tags).toEqual([]);
224
+
225
+ expect(
226
+ await spaceState.process(
227
+ await createCredential({
228
+ issuer: space,
229
+ subject: space,
230
+ assertion: {
231
+ '@type': 'dxos.halo.credentials.SpaceGenesis',
232
+ spaceKey: space,
233
+ tags: ['personal', 'test'],
234
+ membershipPolicy: MembershipPolicy.INVITE,
235
+ },
236
+ signer: keyring,
237
+ }),
238
+ { sourceFeed: feed },
239
+ ),
240
+ ).toEqual(true);
241
+
242
+ expect(spaceState.tags).toEqual(['personal', 'test']);
243
+ });
244
+
245
+ test('space genesis without tags returns empty array', async () => {
246
+ const keyring = new Keyring();
247
+ const space = await keyring.createKey();
248
+ const feed = await keyring.createKey();
249
+
250
+ const spaceState = new SpaceStateMachine(space);
251
+
252
+ await spaceState.process(
253
+ await createCredential({
254
+ issuer: space,
255
+ subject: space,
256
+ assertion: {
257
+ '@type': 'dxos.halo.credentials.SpaceGenesis',
258
+ spaceKey: space,
259
+ membershipPolicy: MembershipPolicy.INVITE,
260
+ },
261
+ signer: keyring,
262
+ }),
263
+ { sourceFeed: feed },
264
+ );
265
+
266
+ expect(spaceState.tags).toEqual([]);
267
+ });
268
+
214
269
  test('storing device credentials and building a chain', async () => {
215
270
  const keyring = new Keyring();
216
271
  const haloSpace = await keyring.createKey();
@@ -230,6 +285,7 @@ describe('SpaceStateMachine', () => {
230
285
  assertion: {
231
286
  '@type': 'dxos.halo.credentials.SpaceGenesis',
232
287
  spaceKey: haloSpace,
288
+ membershipPolicy: MembershipPolicy.INVITE,
233
289
  },
234
290
  signer: keyring,
235
291
  }),
@@ -339,4 +395,189 @@ describe('SpaceStateMachine', () => {
339
395
 
340
396
  expect(await verifyCredential(credential)).toEqual({ kind: 'pass' });
341
397
  });
398
+
399
+ test('space genesis with membership policy', async () => {
400
+ const keyring = new Keyring();
401
+ const space = await keyring.createKey();
402
+ const feed = await keyring.createKey();
403
+
404
+ const spaceState = new SpaceStateMachine(space);
405
+
406
+ await spaceState.process(
407
+ await createCredential({
408
+ issuer: space,
409
+ subject: space,
410
+ assertion: {
411
+ '@type': 'dxos.halo.credentials.SpaceGenesis',
412
+ spaceKey: space,
413
+ membershipPolicy: MembershipPolicy.LOCKED,
414
+ },
415
+ signer: keyring,
416
+ }),
417
+ { sourceFeed: feed },
418
+ );
419
+
420
+ expect(spaceState.membershipPolicy).toEqual(MembershipPolicy.LOCKED);
421
+ });
422
+
423
+ test('space genesis without membership policy defaults to INVITE', async () => {
424
+ const keyring = new Keyring();
425
+ const space = await keyring.createKey();
426
+ const feed = await keyring.createKey();
427
+
428
+ const spaceState = new SpaceStateMachine(space);
429
+
430
+ // MembershipPolicy.INVITE is the proto zero-value default.
431
+ // Existing spaces without the field will have this value.
432
+ await spaceState.process(
433
+ await createCredential({
434
+ issuer: space,
435
+ subject: space,
436
+ assertion: {
437
+ '@type': 'dxos.halo.credentials.SpaceGenesis',
438
+ spaceKey: space,
439
+ membershipPolicy: MembershipPolicy.INVITE,
440
+ },
441
+ signer: keyring,
442
+ }),
443
+ { sourceFeed: feed },
444
+ );
445
+
446
+ expect(spaceState.membershipPolicy).toEqual(MembershipPolicy.INVITE);
447
+ });
448
+
449
+ test('locked space rejects new members', async ({ expect }) => {
450
+ const keyring = new Keyring();
451
+ const space = await keyring.createKey();
452
+ const identity = await keyring.createKey();
453
+ const identity2 = await keyring.createKey();
454
+ const feed = await keyring.createKey();
455
+
456
+ const spaceState = new SpaceStateMachine(space);
457
+
458
+ await spaceState.process(
459
+ await createCredential({
460
+ issuer: space,
461
+ subject: space,
462
+ assertion: {
463
+ '@type': 'dxos.halo.credentials.SpaceGenesis',
464
+ spaceKey: space,
465
+ membershipPolicy: MembershipPolicy.LOCKED,
466
+ },
467
+ signer: keyring,
468
+ }),
469
+ { sourceFeed: feed },
470
+ );
471
+
472
+ await spaceState.process(
473
+ await createCredential({
474
+ issuer: space,
475
+ subject: identity,
476
+ assertion: {
477
+ '@type': 'dxos.halo.credentials.SpaceMember',
478
+ spaceKey: space,
479
+ role: SpaceMember.Role.OWNER,
480
+ genesisFeedKey: feed,
481
+ },
482
+ signer: keyring,
483
+ }),
484
+ { sourceFeed: feed },
485
+ );
486
+
487
+ // Attempt to add a second member — should be rejected.
488
+ expect(
489
+ await spaceState.process(
490
+ await createCredential({
491
+ issuer: space,
492
+ subject: identity2,
493
+ assertion: {
494
+ '@type': 'dxos.halo.credentials.SpaceMember',
495
+ spaceKey: space,
496
+ role: SpaceMember.Role.EDITOR,
497
+ genesisFeedKey: feed,
498
+ },
499
+ signer: keyring,
500
+ }),
501
+ { sourceFeed: feed },
502
+ ),
503
+ ).toEqual(false);
504
+
505
+ expect(spaceState.members.size).toEqual(1);
506
+ });
507
+
508
+ test('locked space allows admitted feeds for existing members', async ({ expect }) => {
509
+ const keyring = new Keyring();
510
+ const space = await keyring.createKey();
511
+ const identity = await keyring.createKey();
512
+ const device = await keyring.createKey();
513
+ const feed = await keyring.createKey();
514
+ const newFeed = await keyring.createKey();
515
+
516
+ const spaceState = new SpaceStateMachine(space);
517
+
518
+ await spaceState.process(
519
+ await createCredential({
520
+ issuer: space,
521
+ subject: space,
522
+ assertion: {
523
+ '@type': 'dxos.halo.credentials.SpaceGenesis',
524
+ spaceKey: space,
525
+ membershipPolicy: MembershipPolicy.LOCKED,
526
+ },
527
+ signer: keyring,
528
+ }),
529
+ { sourceFeed: feed },
530
+ );
531
+
532
+ await spaceState.process(
533
+ await createCredential({
534
+ issuer: space,
535
+ subject: identity,
536
+ assertion: {
537
+ '@type': 'dxos.halo.credentials.SpaceMember',
538
+ spaceKey: space,
539
+ role: SpaceMember.Role.OWNER,
540
+ genesisFeedKey: feed,
541
+ },
542
+ signer: keyring,
543
+ }),
544
+ { sourceFeed: feed },
545
+ );
546
+
547
+ const chain: Chain = {
548
+ credential: await createCredential({
549
+ assertion: {
550
+ '@type': 'dxos.halo.credentials.AuthorizedDevice',
551
+ deviceKey: device,
552
+ identityKey: identity,
553
+ },
554
+ subject: device,
555
+ issuer: identity,
556
+ signer: keyring,
557
+ }),
558
+ };
559
+
560
+ // AdmittedFeed should still work on locked space.
561
+ expect(
562
+ await spaceState.process(
563
+ await createCredential({
564
+ issuer: identity,
565
+ subject: newFeed,
566
+ assertion: {
567
+ '@type': 'dxos.halo.credentials.AdmittedFeed',
568
+ spaceKey: space,
569
+ identityKey: identity,
570
+ deviceKey: device,
571
+ designation: AdmittedFeed.Designation.CONTROL,
572
+ },
573
+ signer: keyring,
574
+ signingKey: device,
575
+ chain,
576
+ }),
577
+ { sourceFeed: feed },
578
+ ),
579
+ ).toEqual(true);
580
+
581
+ expect(spaceState.feeds.size).toEqual(1);
582
+ });
342
583
  });
@@ -7,13 +7,12 @@ import { Context } from '@dxos/context';
7
7
  import { PublicKey } from '@dxos/keys';
8
8
  import { log } from '@dxos/log';
9
9
  import { type TypedMessage } from '@dxos/protocols/proto';
10
- import { type Credential, SpaceMember } from '@dxos/protocols/proto/dxos/halo/credentials';
10
+ import { type Credential, MembershipPolicy, SpaceMember } from '@dxos/protocols/proto/dxos/halo/credentials';
11
11
  import { type DelegateSpaceInvitation } from '@dxos/protocols/proto/dxos/halo/invitations';
12
12
  import { type AsyncCallback, Callback, ComplexMap, ComplexSet } from '@dxos/util';
13
13
 
14
14
  import { getCredentialAssertion, verifyCredential } from '../credentials';
15
15
  import { type CredentialProcessor } from '../processor/credential-processor';
16
-
17
16
  import { type FeedInfo, FeedStateMachine } from './feed-state-machine';
18
17
  import { InvitationStateMachine } from './invitation-state-machine';
19
18
  import { type MemberInfo, MemberStateMachine } from './member-state-machine';
@@ -24,6 +23,8 @@ export interface SpaceState {
24
23
  readonly feeds: ReadonlyMap<PublicKey, FeedInfo>;
25
24
  readonly credentials: Credential[];
26
25
  readonly genesisCredential: Credential | undefined;
26
+ readonly tags: string[];
27
+ readonly membershipPolicy: MembershipPolicy;
27
28
  readonly creator: MemberInfo | undefined;
28
29
  readonly invitations: ReadonlyMap<PublicKey, DelegateSpaceInvitation>;
29
30
 
@@ -61,6 +62,8 @@ export class SpaceStateMachine implements SpaceState {
61
62
  private readonly _processedCredentials = new ComplexSet<PublicKey>(PublicKey.hash);
62
63
 
63
64
  private _genesisCredential: Credential | undefined;
65
+ private _tags: string[] = [];
66
+ private _membershipPolicy: MembershipPolicy = MembershipPolicy.INVITE;
64
67
  private _credentialProcessors: CredentialConsumer<any>[] = [];
65
68
 
66
69
  readonly onCredentialProcessed = new Callback<AsyncCallback<Credential>>();
@@ -104,6 +107,14 @@ export class SpaceStateMachine implements SpaceState {
104
107
  return this._genesisCredential;
105
108
  }
106
109
 
110
+ get tags(): string[] {
111
+ return this._tags;
112
+ }
113
+
114
+ get membershipPolicy(): MembershipPolicy {
115
+ return this._membershipPolicy;
116
+ }
117
+
107
118
  get invitations(): ReadonlyMap<PublicKey, DelegateSpaceInvitation> {
108
119
  return this._invitations.invitations;
109
120
  }
@@ -180,6 +191,8 @@ export class SpaceStateMachine implements SpaceState {
180
191
  return false;
181
192
  }
182
193
  this._genesisCredential = credential;
194
+ this._tags = assertion.tags ?? [];
195
+ this._membershipPolicy = assertion.membershipPolicy ?? MembershipPolicy.INVITE;
183
196
  break;
184
197
  }
185
198
 
@@ -260,6 +273,11 @@ export class SpaceStateMachine implements SpaceState {
260
273
  }
261
274
 
262
275
  private _canInviteNewMembers(key: PublicKey): boolean {
276
+ if (this._membershipPolicy === MembershipPolicy.LOCKED) {
277
+ // When locked, only the space key can add the initial owner during genesis.
278
+ // Once a member exists, no new members can be added.
279
+ return key.equals(this._spaceKey) && this._members.members.size === 0;
280
+ }
263
281
  return (
264
282
  key.equals(this._spaceKey) ||
265
283
  this._members.getRole(key) === SpaceMember.Role.ADMIN ||