@dxos/plugin-client 0.7.5-labs.a279d8c → 0.7.5-labs.d453967

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 (141) hide show
  1. package/dist/lib/browser/{app-graph-builder-6LOTDB4T.mjs → app-graph-builder-JRQVJHEH.mjs} +2 -2
  2. package/dist/lib/browser/chunk-BFNE5KCF.mjs +271 -0
  3. package/dist/lib/browser/chunk-BFNE5KCF.mjs.map +7 -0
  4. package/dist/lib/browser/{chunk-E3OUNOEL.mjs → chunk-GYCVFUTM.mjs} +13 -1
  5. package/dist/lib/browser/{chunk-E3OUNOEL.mjs.map → chunk-GYCVFUTM.mjs.map} +3 -3
  6. package/dist/lib/browser/{chunk-SVBQRT7I.mjs → chunk-MSK3TLIH.mjs} +1 -1
  7. package/dist/lib/browser/{chunk-SVBQRT7I.mjs.map → chunk-MSK3TLIH.mjs.map} +2 -2
  8. package/dist/lib/browser/{chunk-R4Q6H7RD.mjs → chunk-RGMLBYIF.mjs} +2 -2
  9. package/dist/lib/browser/{chunk-R4Q6H7RD.mjs.map → chunk-RGMLBYIF.mjs.map} +2 -2
  10. package/dist/lib/browser/{client-25GYH6TN.mjs → client-BY3RGW3H.mjs} +4 -13
  11. package/dist/lib/browser/client-BY3RGW3H.mjs.map +7 -0
  12. package/dist/lib/browser/index.mjs +27 -14
  13. package/dist/lib/browser/index.mjs.map +3 -3
  14. package/dist/lib/browser/intent-resolver-LWSDA6RM.mjs +263 -0
  15. package/dist/lib/browser/intent-resolver-LWSDA6RM.mjs.map +7 -0
  16. package/dist/lib/browser/meta.json +1 -1
  17. package/dist/lib/browser/{react-context-M3RT6SSG.mjs → react-context-PHX2CVI7.mjs} +2 -2
  18. package/dist/lib/browser/{react-surface-XXONPG5T.mjs → react-surface-6BINNBEX.mjs} +23 -7
  19. package/dist/lib/browser/react-surface-6BINNBEX.mjs.map +7 -0
  20. package/dist/lib/browser/schema-AL34NY2M.mjs +33 -0
  21. package/dist/lib/browser/schema-AL34NY2M.mjs.map +7 -0
  22. package/dist/lib/browser/types.mjs +1 -1
  23. package/dist/lib/node/{app-graph-builder-JCND3P6G.cjs → app-graph-builder-R6QR2VLZ.cjs} +6 -6
  24. package/dist/lib/node/chunk-3P5BBMXO.cjs +299 -0
  25. package/dist/lib/node/chunk-3P5BBMXO.cjs.map +7 -0
  26. package/dist/lib/node/{chunk-NVYWMDF3.cjs → chunk-NZKGFE46.cjs} +16 -4
  27. package/dist/lib/node/{chunk-NVYWMDF3.cjs.map → chunk-NZKGFE46.cjs.map} +3 -3
  28. package/dist/lib/node/{chunk-IXVWJCYB.cjs → chunk-XPC4JOPP.cjs} +5 -5
  29. package/dist/lib/node/{chunk-IXVWJCYB.cjs.map → chunk-XPC4JOPP.cjs.map} +2 -2
  30. package/dist/lib/node/{chunk-3OB25QN2.cjs → chunk-ZI7AV5RP.cjs} +4 -4
  31. package/dist/lib/node/{chunk-3OB25QN2.cjs.map → chunk-ZI7AV5RP.cjs.map} +2 -2
  32. package/dist/lib/node/{client-52L4TUYU.cjs → client-JCI7A3IV.cjs} +9 -18
  33. package/dist/lib/node/client-JCI7A3IV.cjs.map +7 -0
  34. package/dist/lib/node/index.cjs +29 -16
  35. package/dist/lib/node/index.cjs.map +3 -3
  36. package/dist/lib/node/intent-resolver-ODZVEAZ2.cjs +271 -0
  37. package/dist/lib/node/intent-resolver-ODZVEAZ2.cjs.map +7 -0
  38. package/dist/lib/node/meta.json +1 -1
  39. package/dist/lib/node/{react-context-B2DNULH4.cjs → react-context-WMOFXPT4.cjs} +6 -6
  40. package/dist/lib/node/{react-surface-7USN5NOO.cjs → react-surface-KRKGFWS3.cjs} +28 -16
  41. package/dist/lib/node/react-surface-KRKGFWS3.cjs.map +7 -0
  42. package/dist/lib/node/schema-HW4W3QYU.cjs +49 -0
  43. package/dist/lib/node/schema-HW4W3QYU.cjs.map +7 -0
  44. package/dist/lib/node/types.cjs +2 -2
  45. package/dist/lib/node/types.cjs.map +1 -1
  46. package/dist/lib/node-esm/{app-graph-builder-ZLGKFBLY.mjs → app-graph-builder-WXXGCOJB.mjs} +2 -2
  47. package/dist/lib/node-esm/chunk-2FPF25KT.mjs +272 -0
  48. package/dist/lib/node-esm/chunk-2FPF25KT.mjs.map +7 -0
  49. package/dist/lib/node-esm/{chunk-OBGRL3DF.mjs → chunk-B73LKPGU.mjs} +1 -1
  50. package/dist/lib/node-esm/{chunk-OBGRL3DF.mjs.map → chunk-B73LKPGU.mjs.map} +2 -2
  51. package/dist/lib/node-esm/{chunk-74T33KTB.mjs → chunk-M2NR7HNG.mjs} +13 -1
  52. package/dist/lib/node-esm/{chunk-74T33KTB.mjs.map → chunk-M2NR7HNG.mjs.map} +3 -3
  53. package/dist/lib/node-esm/{chunk-73M5MK5Q.mjs → chunk-QR72BXJK.mjs} +2 -2
  54. package/dist/lib/node-esm/{chunk-73M5MK5Q.mjs.map → chunk-QR72BXJK.mjs.map} +2 -2
  55. package/dist/lib/node-esm/{client-K52NWOUN.mjs → client-XWK653HB.mjs} +4 -13
  56. package/dist/lib/node-esm/client-XWK653HB.mjs.map +7 -0
  57. package/dist/lib/node-esm/index.mjs +27 -14
  58. package/dist/lib/node-esm/index.mjs.map +3 -3
  59. package/dist/lib/node-esm/intent-resolver-IHYGJREB.mjs +264 -0
  60. package/dist/lib/node-esm/intent-resolver-IHYGJREB.mjs.map +7 -0
  61. package/dist/lib/node-esm/meta.json +1 -1
  62. package/dist/lib/node-esm/{react-context-IGEHJQK5.mjs → react-context-EEGDXBWA.mjs} +2 -2
  63. package/dist/lib/node-esm/{react-surface-CXLBCMPN.mjs → react-surface-OQFDVSAP.mjs} +23 -7
  64. package/dist/lib/node-esm/react-surface-OQFDVSAP.mjs.map +7 -0
  65. package/dist/lib/node-esm/schema-74LVJ3EU.mjs +34 -0
  66. package/dist/lib/node-esm/schema-74LVJ3EU.mjs.map +7 -0
  67. package/dist/lib/node-esm/types.mjs +1 -1
  68. package/dist/types/src/ClientPlugin.d.ts.map +1 -1
  69. package/dist/types/src/capabilities/app-graph-builder.d.ts +22 -22
  70. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  71. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -1
  72. package/dist/types/src/capabilities/client.d.ts.map +1 -1
  73. package/dist/types/src/capabilities/index.d.ts +31 -29
  74. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  75. package/dist/types/src/capabilities/intent-resolver.d.ts +2 -1
  76. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -1
  77. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  78. package/dist/types/src/capabilities/schema.d.ts +4 -0
  79. package/dist/types/src/capabilities/schema.d.ts.map +1 -0
  80. package/dist/types/src/components/IdentityDialog.d.ts.map +1 -1
  81. package/dist/types/src/components/ManageCredentialsDialog.d.ts +4 -0
  82. package/dist/types/src/components/ManageCredentialsDialog.d.ts.map +1 -0
  83. package/dist/types/src/components/ManageCredentialsDialog.stories.d.ts +8 -0
  84. package/dist/types/src/components/ManageCredentialsDialog.stories.d.ts.map +1 -0
  85. package/dist/types/src/components/RecoveryCodeDialog.d.ts.map +1 -1
  86. package/dist/types/src/components/RecoverySetupDialog.d.ts +4 -0
  87. package/dist/types/src/components/RecoverySetupDialog.d.ts.map +1 -0
  88. package/dist/types/src/components/RecoverySetupDialog.stories.d.ts +8 -0
  89. package/dist/types/src/components/RecoverySetupDialog.stories.d.ts.map +1 -0
  90. package/dist/types/src/components/index.d.ts +1 -0
  91. package/dist/types/src/components/index.d.ts.map +1 -1
  92. package/dist/types/src/events.d.ts +1 -1
  93. package/dist/types/src/translations.d.ts +7 -0
  94. package/dist/types/src/translations.d.ts.map +1 -1
  95. package/dist/types/src/types.d.ts +16 -0
  96. package/dist/types/src/types.d.ts.map +1 -1
  97. package/dist/types/tsconfig.tsbuildinfo +1 -1
  98. package/package.json +22 -17
  99. package/src/ClientPlugin.ts +10 -5
  100. package/src/capabilities/capabilities.ts +1 -0
  101. package/src/capabilities/client.ts +1 -9
  102. package/src/capabilities/index.ts +1 -0
  103. package/src/capabilities/intent-resolver.ts +164 -67
  104. package/src/capabilities/react-surface.tsx +21 -6
  105. package/src/capabilities/schema.ts +36 -0
  106. package/src/components/IdentityDialog.tsx +15 -3
  107. package/src/components/JoinDialog.tsx +1 -1
  108. package/src/components/ManageCredentialsDialog.stories.tsx +57 -0
  109. package/src/components/ManageCredentialsDialog.tsx +64 -0
  110. package/src/components/RecoveryCodeDialog.stories.tsx +5 -5
  111. package/src/components/RecoveryCodeDialog.tsx +2 -1
  112. package/src/components/RecoverySetupDialog.stories.tsx +77 -0
  113. package/src/components/RecoverySetupDialog.tsx +52 -0
  114. package/src/components/index.ts +1 -0
  115. package/src/events.ts +1 -1
  116. package/src/translations.ts +10 -1
  117. package/src/types.ts +10 -0
  118. package/dist/lib/browser/chunk-6IF6PREG.mjs +0 -158
  119. package/dist/lib/browser/chunk-6IF6PREG.mjs.map +0 -7
  120. package/dist/lib/browser/client-25GYH6TN.mjs.map +0 -7
  121. package/dist/lib/browser/intent-resolver-IWWLWOZD.mjs +0 -132
  122. package/dist/lib/browser/intent-resolver-IWWLWOZD.mjs.map +0 -7
  123. package/dist/lib/browser/react-surface-XXONPG5T.mjs.map +0 -7
  124. package/dist/lib/node/chunk-N3U7KVO3.cjs +0 -186
  125. package/dist/lib/node/chunk-N3U7KVO3.cjs.map +0 -7
  126. package/dist/lib/node/client-52L4TUYU.cjs.map +0 -7
  127. package/dist/lib/node/intent-resolver-H3Y64MIP.cjs +0 -140
  128. package/dist/lib/node/intent-resolver-H3Y64MIP.cjs.map +0 -7
  129. package/dist/lib/node/react-surface-7USN5NOO.cjs.map +0 -7
  130. package/dist/lib/node-esm/chunk-EHGD3UJH.mjs +0 -159
  131. package/dist/lib/node-esm/chunk-EHGD3UJH.mjs.map +0 -7
  132. package/dist/lib/node-esm/client-K52NWOUN.mjs.map +0 -7
  133. package/dist/lib/node-esm/intent-resolver-JCKD6R6L.mjs +0 -133
  134. package/dist/lib/node-esm/intent-resolver-JCKD6R6L.mjs.map +0 -7
  135. package/dist/lib/node-esm/react-surface-CXLBCMPN.mjs.map +0 -7
  136. /package/dist/lib/browser/{app-graph-builder-6LOTDB4T.mjs.map → app-graph-builder-JRQVJHEH.mjs.map} +0 -0
  137. /package/dist/lib/browser/{react-context-M3RT6SSG.mjs.map → react-context-PHX2CVI7.mjs.map} +0 -0
  138. /package/dist/lib/node/{app-graph-builder-JCND3P6G.cjs.map → app-graph-builder-R6QR2VLZ.cjs.map} +0 -0
  139. /package/dist/lib/node/{react-context-B2DNULH4.cjs.map → react-context-WMOFXPT4.cjs.map} +0 -0
  140. /package/dist/lib/node-esm/{app-graph-builder-ZLGKFBLY.mjs.map → app-graph-builder-WXXGCOJB.mjs.map} +0 -0
  141. /package/dist/lib/node-esm/{react-context-IGEHJQK5.mjs.map → react-context-EEGDXBWA.mjs.map} +0 -0
@@ -12,6 +12,7 @@ import {
12
12
  } from '@dxos/app-framework';
13
13
  import { invariant } from '@dxos/invariant';
14
14
  import { ObservabilityAction } from '@dxos/plugin-observability/types';
15
+ import { PublicKey } from '@dxos/react-client';
15
16
  import { type JoinPanelProps } from '@dxos/shell/react';
16
17
 
17
18
  import { ClientCapabilities } from './capabilities';
@@ -21,81 +22,177 @@ import { ClientAction, type ClientPluginOptions } from '../types';
21
22
 
22
23
  type IntentResolverOptions = Pick<ClientPluginOptions, 'onReset'> & {
23
24
  context: PluginsContext;
25
+ appName?: string;
24
26
  };
25
27
 
26
- export default ({ context, onReset }: IntentResolverOptions) =>
28
+ export default ({ context, appName = 'Composer', onReset }: IntentResolverOptions) =>
27
29
  contributes(Capabilities.IntentResolver, [
28
- createResolver(ClientAction.CreateIdentity, async () => {
29
- const manager = context.requestCapability(Capabilities.PluginManager);
30
- const client = context.requestCapability(ClientCapabilities.Client);
31
- const data = await client.halo.createIdentity();
32
- await manager.activate(ClientEvents.IdentityCreated);
33
- return { data, intents: [createIntent(ObservabilityAction.SendEvent, { name: 'identity.create' })] };
30
+ createResolver({
31
+ intent: ClientAction.CreateIdentity,
32
+ resolve: async () => {
33
+ const manager = context.requestCapability(Capabilities.PluginManager);
34
+ const client = context.requestCapability(ClientCapabilities.Client);
35
+ const data = await client.halo.createIdentity();
36
+ await manager.activate(ClientEvents.IdentityCreated);
37
+ return { data, intents: [createIntent(ObservabilityAction.SendEvent, { name: 'identity.create' })] };
38
+ },
34
39
  }),
35
- createResolver(ClientAction.JoinIdentity, async (data) => {
36
- return {
37
- intents: [
38
- createIntent(LayoutAction.SetLayout, {
39
- element: 'dialog',
40
- component: JOIN_DIALOG,
41
- dialogBlockAlign: 'start',
42
- subject: {
43
- initialInvitationCode: data.invitationCode,
44
- initialDisposition: 'accept-halo-invitation',
45
- } satisfies Partial<JoinPanelProps>,
46
- }),
47
- ],
48
- };
40
+ createResolver({
41
+ intent: ClientAction.JoinIdentity,
42
+ resolve: async (data) => {
43
+ return {
44
+ intents: [
45
+ createIntent(LayoutAction.UpdateDialog, {
46
+ part: 'dialog',
47
+ subject: JOIN_DIALOG,
48
+ options: {
49
+ blockAlign: 'start',
50
+ props: {
51
+ initialInvitationCode: data.invitationCode,
52
+ initialDisposition: 'accept-halo-invitation',
53
+ },
54
+ },
55
+ }),
56
+ ],
57
+ };
58
+ },
49
59
  }),
50
- createResolver(ClientAction.ShareIdentity, async () => {
51
- return {
52
- intents: [
53
- createIntent(LayoutAction.SetLayout, {
54
- element: 'dialog',
55
- component: IDENTITY_DIALOG,
56
- dialogBlockAlign: 'start',
57
- }),
58
- createIntent(ObservabilityAction.SendEvent, { name: 'identity.share' }),
59
- ],
60
- };
60
+ createResolver({
61
+ intent: ClientAction.ShareIdentity,
62
+ resolve: async () => {
63
+ return {
64
+ intents: [
65
+ createIntent(LayoutAction.UpdateDialog, {
66
+ part: 'dialog',
67
+ subject: IDENTITY_DIALOG,
68
+ options: {
69
+ blockAlign: 'start',
70
+ },
71
+ }),
72
+ createIntent(ObservabilityAction.SendEvent, { name: 'identity.share' }),
73
+ ],
74
+ };
75
+ },
61
76
  }),
62
- createResolver(ClientAction.RecoverIdentity, async () => {
63
- return {
64
- intents: [
65
- createIntent(LayoutAction.SetLayout, {
66
- element: 'dialog',
67
- component: JOIN_DIALOG,
68
- dialogBlockAlign: 'start',
69
- subject: { initialDisposition: 'recover-identity' } satisfies Partial<JoinPanelProps>,
70
- }),
71
- ],
72
- };
77
+ createResolver({
78
+ intent: ClientAction.RecoverIdentity,
79
+ resolve: async () => {
80
+ return {
81
+ intents: [
82
+ createIntent(LayoutAction.UpdateDialog, {
83
+ part: 'dialog',
84
+ subject: JOIN_DIALOG,
85
+ options: {
86
+ blockAlign: 'start',
87
+ props: {
88
+ initialDisposition: 'recover-identity',
89
+ } satisfies Partial<JoinPanelProps>,
90
+ },
91
+ }),
92
+ ],
93
+ };
94
+ },
73
95
  }),
74
- createResolver(ClientAction.ResetStorage, async (data) => {
75
- await onReset?.({ target: data.target });
76
- return {};
96
+ createResolver({
97
+ intent: ClientAction.ResetStorage,
98
+ resolve: async (data) => {
99
+ await onReset?.({ target: data.target });
100
+ },
77
101
  }),
78
- createResolver(ClientAction.CreateAgent, async () => {
79
- const client = context.requestCapability(ClientCapabilities.Client);
80
- invariant(client.services.services.EdgeAgentService, 'Missing EdgeAgentService');
81
- await client.services.services.EdgeAgentService.createAgent(null as any, { timeout: 10_000 });
102
+ createResolver({
103
+ intent: ClientAction.CreateAgent,
104
+ resolve: async () => {
105
+ const client = context.requestCapability(ClientCapabilities.Client);
106
+ invariant(client.services.services.EdgeAgentService, 'Missing EdgeAgentService');
107
+ await client.services.services.EdgeAgentService.createAgent(null as any, { timeout: 10_000 });
108
+ },
82
109
  }),
83
- createResolver(ClientAction.CreateRecoveryCode, async () => {
84
- const client = context.requestCapability(ClientCapabilities.Client);
85
- invariant(client.services.services.IdentityService, 'IdentityService not available');
86
- // TODO(wittjosiah): This needs a proper api. Rename property.
87
- const { seedphrase } = await client.services.services.IdentityService.createRecoveryPhrase();
88
- return {
89
- intents: [
90
- createIntent(LayoutAction.SetLayout, {
91
- element: 'dialog',
92
- dialogBlockAlign: 'start',
93
- dialogType: 'alert',
94
- state: true,
95
- component: RECOVER_CODE_DIALOG,
96
- subject: { code: seedphrase },
97
- }),
98
- ],
99
- };
110
+ createResolver({
111
+ intent: ClientAction.CreateRecoveryCode,
112
+ resolve: async () => {
113
+ const client = context.requestCapability(ClientCapabilities.Client);
114
+ invariant(client.services.services.IdentityService, 'IdentityService not available');
115
+ // TODO(wittjosiah): This needs a proper api.
116
+ const { recoveryCode } = await client.services.services.IdentityService.createRecoveryCredential({});
117
+ return {
118
+ intents: [
119
+ createIntent(LayoutAction.UpdateDialog, {
120
+ part: 'dialog',
121
+ subject: RECOVER_CODE_DIALOG,
122
+ options: {
123
+ blockAlign: 'start',
124
+ type: 'alert',
125
+ props: { code: recoveryCode },
126
+ },
127
+ }),
128
+ ],
129
+ };
130
+ },
131
+ }),
132
+ createResolver({
133
+ intent: ClientAction.CreatePasskey,
134
+ resolve: async () => {
135
+ const client = context.requestCapability(ClientCapabilities.Client);
136
+ const identity = client.halo.identity.get();
137
+ invariant(identity, 'Identity not available');
138
+
139
+ // TODO(wittjosiah): Consider factoring out passkey creation to the halo api.
140
+ const credential = await navigator.credentials.create({
141
+ publicKey: {
142
+ challenge: new Uint8Array(),
143
+ rp: { id: location.hostname, name: appName },
144
+ user: {
145
+ id: new TextEncoder().encode(identity.did),
146
+ name: identity.did,
147
+ displayName: identity.profile?.displayName ?? '',
148
+ },
149
+ pubKeyCredParams: [
150
+ { type: 'public-key', alg: -8 }, // Ed25519 (not yet supported across all browsers)
151
+ { type: 'public-key', alg: -7 }, // ES256
152
+ ],
153
+ // https://web.dev/articles/webauthn-discoverable-credentials#resident-key
154
+ authenticatorSelection: {
155
+ residentKey: 'required',
156
+ requireResidentKey: true,
157
+ },
158
+ },
159
+ });
160
+
161
+ invariant(credential, 'Credential not available');
162
+ const recoveryKey = PublicKey.from(new Uint8Array((credential as any).response.getPublicKey()));
163
+ const algorithm = (credential as any).response.getPublicKeyAlgorithm() === -7 ? 'ES256' : 'ED25519';
164
+
165
+ // TODO(wittjosiah): This needs a proper api.
166
+ invariant(client.services.services.IdentityService, 'IdentityService not available');
167
+ await client.services.services.IdentityService.createRecoveryCredential({ recoveryKey, algorithm });
168
+ },
169
+ }),
170
+ createResolver({
171
+ intent: ClientAction.RedeemPasskey,
172
+ resolve: async () => {
173
+ const client = context.requestCapability(ClientCapabilities.Client);
174
+ // TODO(wittjosiah): This needs a proper api.
175
+ invariant(client.services.services.IdentityService, 'IdentityService not available');
176
+ const { deviceKey, controlFeedKey, challenge } =
177
+ await client.services.services.IdentityService.requestRecoveryChallenge();
178
+ const credential = await navigator.credentials.get({
179
+ publicKey: {
180
+ challenge: Buffer.from(challenge, 'base64'),
181
+ rpId: location.hostname,
182
+ userVerification: 'required',
183
+ },
184
+ });
185
+ const identityDid = new TextDecoder().decode((credential as any).response.userHandle);
186
+ await client.services.services.IdentityService.recoverIdentity({
187
+ external: {
188
+ identityDid,
189
+ deviceKey,
190
+ controlFeedKey,
191
+ signature: Buffer.from((credential as any).response.signature),
192
+ clientDataJson: Buffer.from((credential as any).response.clientDataJSON),
193
+ authenticatorData: Buffer.from((credential as any).response.authenticatorData),
194
+ },
195
+ });
196
+ },
100
197
  }),
101
198
  ]);
@@ -13,9 +13,12 @@ import {
13
13
  JOIN_DIALOG,
14
14
  JoinDialog,
15
15
  RECOVER_CODE_DIALOG,
16
+ RECOVER_SETUP_DIALOG,
16
17
  RecoveryCodeDialog,
18
+ RecoverySetupDialog,
17
19
  type RecoveryCodeDialogProps,
18
20
  } from '../components';
21
+ import { MANAGE_CREDENTIALS_DIALOG, ManageCredentialsDialog } from '../components/ManageCredentialsDialog';
19
22
 
20
23
  type ReactSurfaceOptions = {
21
24
  createInvitationUrl: (invitationCode: string) => string;
@@ -26,19 +29,31 @@ export default ({ createInvitationUrl }: ReactSurfaceOptions) =>
26
29
  createSurface({
27
30
  id: IDENTITY_DIALOG,
28
31
  role: 'dialog',
29
- filter: (data): data is { subject: IdentityPanelProps } => data.component === IDENTITY_DIALOG,
30
- component: ({ data }) => <IdentityDialog {...data.subject} createInvitationUrl={createInvitationUrl} />,
32
+ filter: (data): data is { props: IdentityPanelProps } => data.component === IDENTITY_DIALOG,
33
+ component: ({ data }) => <IdentityDialog {...data.props} createInvitationUrl={createInvitationUrl} />,
31
34
  }),
32
35
  createSurface({
33
36
  id: JOIN_DIALOG,
34
37
  role: 'dialog',
35
- filter: (data): data is { subject: JoinPanelProps } => data.component === JOIN_DIALOG,
36
- component: ({ data }) => <JoinDialog {...data.subject} />,
38
+ filter: (data): data is { props: JoinPanelProps } => data.component === JOIN_DIALOG,
39
+ component: ({ data }) => <JoinDialog {...data.props} />,
37
40
  }),
38
41
  createSurface({
39
42
  id: RECOVER_CODE_DIALOG,
40
43
  role: 'dialog',
41
- filter: (data): data is { subject: RecoveryCodeDialogProps } => data.component === RECOVER_CODE_DIALOG,
42
- component: ({ data }) => <RecoveryCodeDialog {...data.subject} />,
44
+ filter: (data): data is { props: RecoveryCodeDialogProps } => data.component === RECOVER_CODE_DIALOG,
45
+ component: ({ data }) => <RecoveryCodeDialog {...data.props} />,
46
+ }),
47
+ createSurface({
48
+ id: RECOVER_SETUP_DIALOG,
49
+ role: 'dialog',
50
+ filter: (data): data is any => data.component === RECOVER_SETUP_DIALOG,
51
+ component: () => <RecoverySetupDialog />,
52
+ }),
53
+ createSurface({
54
+ id: MANAGE_CREDENTIALS_DIALOG,
55
+ role: 'dialog',
56
+ filter: (data): data is any => data.component === MANAGE_CREDENTIALS_DIALOG,
57
+ component: () => <ManageCredentialsDialog />,
43
58
  }),
44
59
  ]);
@@ -0,0 +1,36 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { effect } from '@preact/signals-core';
6
+
7
+ import { Capabilities, contributes, type PluginsContext } from '@dxos/app-framework';
8
+ import { type TypedObject } from '@dxos/echo-schema';
9
+
10
+ import { ClientCapabilities } from './capabilities';
11
+
12
+ // TODO(wittjosiah): Remove types?
13
+ export default (context: PluginsContext) => {
14
+ const client = context.requestCapability(ClientCapabilities.Client);
15
+
16
+ let previousSystem: TypedObject[] = [];
17
+ const unsubscribeSystem = effect(() => {
18
+ const systemSchemas = Array.from(new Set(context.requestCapabilities(ClientCapabilities.SystemSchema).flat()));
19
+ const newSchemas = systemSchemas.filter((schema) => !previousSystem.includes(schema));
20
+ previousSystem = systemSchemas;
21
+ client.addTypes(newSchemas);
22
+ });
23
+
24
+ let previous: TypedObject[] = [];
25
+ const unsubscribe = effect(() => {
26
+ const schemas = Array.from(new Set(context.requestCapabilities(ClientCapabilities.Schema).flat()));
27
+ const newSchemas = schemas.filter((schema) => !previous.includes(schema));
28
+ previous = schemas;
29
+ client.addTypes(newSchemas);
30
+ });
31
+
32
+ return contributes(Capabilities.Null, null, () => {
33
+ unsubscribeSystem();
34
+ unsubscribe();
35
+ });
36
+ };
@@ -9,6 +9,7 @@ import { useClient } from '@dxos/react-client';
9
9
  import { Clipboard, Dialog } from '@dxos/react-ui';
10
10
  import { IdentityPanel, type IdentityPanelProps } from '@dxos/shell/react';
11
11
 
12
+ import { MANAGE_CREDENTIALS_DIALOG } from './ManageCredentialsDialog';
12
13
  import { CLIENT_PLUGIN } from '../meta';
13
14
  import { ClientAction } from '../types';
14
15
 
@@ -21,9 +22,9 @@ export const IdentityDialog = (props: IdentityPanelProps) => {
21
22
  const handleDone = useCallback(
22
23
  () =>
23
24
  dispatch(
24
- createIntent(LayoutAction.SetLayout, {
25
- element: 'dialog',
26
- state: false,
25
+ createIntent(LayoutAction.UpdateDialog, {
26
+ part: 'dialog',
27
+ options: { state: false },
27
28
  }),
28
29
  ),
29
30
  [dispatch],
@@ -44,6 +45,16 @@ export const IdentityDialog = (props: IdentityPanelProps) => {
44
45
  await dispatch(createIntent(ClientAction.ResetStorage, { target: 'deviceInvitation' }));
45
46
  }, [dispatch]);
46
47
 
48
+ const handleManageCredentials = useCallback(async () => {
49
+ await dispatch(
50
+ createIntent(LayoutAction.UpdateDialog, {
51
+ part: 'dialog',
52
+ subject: MANAGE_CREDENTIALS_DIALOG,
53
+ options: { state: true, blockAlign: 'start' },
54
+ }),
55
+ );
56
+ }, [dispatch]);
57
+
47
58
  return (
48
59
  <Dialog.Content>
49
60
  <Clipboard.Provider>
@@ -54,6 +65,7 @@ export const IdentityDialog = (props: IdentityPanelProps) => {
54
65
  onResetStorage={handleResetStorage}
55
66
  onRecover={handleRecover}
56
67
  onJoinNewIdentity={handleJoinNewIdentity}
68
+ onManageCredentials={handleManageCredentials}
57
69
  />
58
70
  </Clipboard.Provider>
59
71
  </Dialog.Content>
@@ -24,7 +24,7 @@ export const JoinDialog = (props: JoinPanelProps) => {
24
24
  async (result: InvitationResult | null) => {
25
25
  if (result?.identityKey) {
26
26
  await Promise.all([
27
- dispatch(createIntent(LayoutAction.SetLayout, { element: 'dialog', state: false })),
27
+ dispatch(createIntent(LayoutAction.UpdateDialog, { part: 'dialog', options: { state: false } })),
28
28
  dispatch(
29
29
  createIntent(ObservabilityAction.SendEvent, {
30
30
  name: props.initialDisposition === 'recover-identity' ? 'identity.recover' : 'identity.join',
@@ -0,0 +1,57 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type StoryObj, type Meta } from '@storybook/react';
8
+ import React from 'react';
9
+
10
+ import { IntentPlugin } from '@dxos/app-framework';
11
+ import { withPluginManager } from '@dxos/app-framework/testing';
12
+ import { AlertDialog } from '@dxos/react-ui';
13
+ import { withTheme, withLayout } from '@dxos/storybook-utils';
14
+
15
+ import { ManageCredentialsDialog } from './ManageCredentialsDialog';
16
+ import { ClientPlugin } from '../ClientPlugin';
17
+ import translations from '../translations';
18
+
19
+ const Render = () => {
20
+ return (
21
+ <AlertDialog.Root open>
22
+ <AlertDialog.Overlay>
23
+ <ManageCredentialsDialog />
24
+ </AlertDialog.Overlay>
25
+ </AlertDialog.Root>
26
+ );
27
+ };
28
+
29
+ const meta: Meta = {
30
+ title: 'plugins/plugin-client/ManageCredentialsDialog',
31
+ component: ManageCredentialsDialog,
32
+ render: Render,
33
+ decorators: [
34
+ withPluginManager({
35
+ plugins: [
36
+ IntentPlugin(),
37
+ ClientPlugin({
38
+ onClientInitialized: async (_, client) => {
39
+ await client.halo.createIdentity();
40
+ },
41
+ }),
42
+ ],
43
+ }),
44
+ withTheme,
45
+ withLayout({ tooltips: true }),
46
+ ],
47
+ parameters: {
48
+ layout: 'fullscreen',
49
+ translations,
50
+ },
51
+ };
52
+
53
+ export default meta;
54
+
55
+ type Story = StoryObj<typeof ManageCredentialsDialog>;
56
+
57
+ export const Default: Story = {};
@@ -0,0 +1,64 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Receipt, Key } from '@phosphor-icons/react';
6
+ import React, { useMemo } from 'react';
7
+
8
+ import { createIntent, useIntentDispatcher } from '@dxos/app-framework';
9
+ import { useCredentials } from '@dxos/react-client/halo';
10
+ import { Dialog, Icon, List, ListItem, useTranslation } from '@dxos/react-ui';
11
+ import { BifurcatedAction, type ActionMenuItem } from '@dxos/shell/react';
12
+
13
+ import { CLIENT_PLUGIN } from '../meta';
14
+ import { ClientAction } from '../types';
15
+
16
+ export const MANAGE_CREDENTIALS_DIALOG = `${CLIENT_PLUGIN}/ManageCredentialsDialog`;
17
+
18
+ // TODO(wittjosiah): Factor panel out to @dxos/shell.
19
+ export const ManageCredentialsDialog = () => {
20
+ const { t } = useTranslation(CLIENT_PLUGIN);
21
+ const { dispatchPromise: dispatch } = useIntentDispatcher();
22
+ const credentials = useCredentials();
23
+ const recoveryCredentials = credentials.filter(
24
+ (credential) => credential.subject.assertion['@type'] === 'dxos.halo.credentials.IdentityRecovery',
25
+ );
26
+
27
+ // TODO(wittjosiah): Reconsile w/ RecoverySetupDialog actions.
28
+ const actions: Record<string, ActionMenuItem> = useMemo(
29
+ () => ({
30
+ createPasskey: {
31
+ label: t('create passkey label'),
32
+ description: t('create passkey description'),
33
+ // TODO(wittjosiah): Ideally this would be a `user-key` icon.
34
+ icon: Key,
35
+ onClick: () => dispatch(createIntent(ClientAction.CreatePasskey)),
36
+ },
37
+ createRecoveryCode: {
38
+ label: t('create recovery code label'),
39
+ description: t('create recovery code description'),
40
+ icon: Receipt,
41
+ onClick: () => dispatch(createIntent(ClientAction.CreateRecoveryCode)),
42
+ },
43
+ }),
44
+ [t],
45
+ );
46
+
47
+ return (
48
+ <Dialog.Content classNames='bs-content min-bs-[15rem] max-bs-full md:max-is-[25rem] overflow-hidden'>
49
+ <Dialog.Title>{t('manage credentials dialog title')}</Dialog.Title>
50
+ <List classNames='py-4'>
51
+ {recoveryCredentials.map((credential) => (
52
+ <ListItem.Root key={credential.id?.toHex()}>
53
+ <ListItem.Endcap>
54
+ <Icon icon='ph--key--regular' size={5} />
55
+ </ListItem.Endcap>
56
+ <ListItem.Heading>{credential.issuanceDate.toLocaleString()}</ListItem.Heading>
57
+ </ListItem.Root>
58
+ ))}
59
+ </List>
60
+ <div className='grow' />
61
+ <BifurcatedAction actions={actions} />
62
+ </Dialog.Content>
63
+ );
64
+ };
@@ -17,16 +17,16 @@ import translations from '../translations';
17
17
 
18
18
  const Render = () => {
19
19
  const client = useClient();
20
- const [seedphrase, setSeedphrase] = useState<string>();
20
+ const [recoveryCode, setRecoveryCode] = useState<string>();
21
21
  useAsyncEffect(async () => {
22
- const { seedphrase } = (await client.services.services.IdentityService?.createRecoveryPhrase()) ?? {};
23
- setSeedphrase(seedphrase);
22
+ const { recoveryCode } = (await client.services.services.IdentityService?.createRecoveryCredential({})) ?? {};
23
+ setRecoveryCode(recoveryCode);
24
24
  }, [client]);
25
25
 
26
26
  return (
27
- <AlertDialog.Root open={!!seedphrase}>
27
+ <AlertDialog.Root open={!!recoveryCode}>
28
28
  <AlertDialog.Overlay>
29
- <RecoveryCodeDialog code={seedphrase ?? ''} />
29
+ <RecoveryCodeDialog code={recoveryCode ?? ''} />
30
30
  </AlertDialog.Overlay>
31
31
  </AlertDialog.Root>
32
32
  );
@@ -14,6 +14,7 @@ export type RecoveryCodeDialogProps = {
14
14
  code: string;
15
15
  };
16
16
 
17
+ // TODO(wittjosiah): Factor panel out to @dxos/shell.
17
18
  export const RecoveryCodeDialog = ({ code }: RecoveryCodeDialogProps) => {
18
19
  const { t } = useTranslation(CLIENT_PLUGIN);
19
20
  const [confirmation, setConfirmation] = useState(false);
@@ -59,7 +60,7 @@ const Code = ({ code }: { code: string }) => {
59
60
  <Clipboard.IconButton value={code} classNames='absolute top-2 right-2 invisible group-hover:visible' />
60
61
  <div className='grid grid-cols-4'>
61
62
  {words.map((word, i) => (
62
- <div key={i} className='flex items-center p-2 gap-2 items-center'>
63
+ <div key={i} className='flex items-center p-2 gap-2'>
63
64
  <div className='w-4 text-xs text-center text-subdued'>{i + 1}</div>
64
65
  <div className='text-sm'>{word}</div>
65
66
  </div>
@@ -0,0 +1,77 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type StoryObj, type Meta } from '@storybook/react';
8
+ import React from 'react';
9
+
10
+ import { createResolver, contributes, Capabilities, IntentPlugin, LayoutAction } from '@dxos/app-framework';
11
+ import { withPluginManager } from '@dxos/app-framework/testing';
12
+ import { S } from '@dxos/echo-schema';
13
+ import { create } from '@dxos/live-object';
14
+ import { AlertDialog } from '@dxos/react-ui';
15
+ import { withTheme, withLayout } from '@dxos/storybook-utils';
16
+
17
+ import { RECOVER_CODE_DIALOG, RecoveryCodeDialog } from './RecoveryCodeDialog';
18
+ import { RecoverySetupDialog } from './RecoverySetupDialog';
19
+ import { ClientPlugin } from '../ClientPlugin';
20
+ import translations from '../translations';
21
+
22
+ const state = create<{ recoveryCode?: string }>({});
23
+
24
+ const Render = () => {
25
+ return (
26
+ <AlertDialog.Root open>
27
+ <AlertDialog.Overlay>
28
+ {state.recoveryCode ? <RecoveryCodeDialog code={state.recoveryCode} /> : <RecoverySetupDialog />}
29
+ </AlertDialog.Overlay>
30
+ </AlertDialog.Root>
31
+ );
32
+ };
33
+
34
+ const meta: Meta = {
35
+ title: 'plugins/plugin-client/RecoverySetupDialog',
36
+ component: RecoverySetupDialog,
37
+ render: Render,
38
+ decorators: [
39
+ withPluginManager({
40
+ plugins: [
41
+ IntentPlugin(),
42
+ ClientPlugin({
43
+ onClientInitialized: async (_, client) => {
44
+ await client.halo.createIdentity();
45
+ },
46
+ }),
47
+ ],
48
+ capabilities: [
49
+ contributes(
50
+ Capabilities.IntentResolver,
51
+ createResolver({
52
+ intent: LayoutAction.UpdateLayout,
53
+ filter: (data): data is S.Schema.Type<typeof LayoutAction.UpdateDialog.fields.input> =>
54
+ S.is(LayoutAction.UpdateDialog.fields.input)(data),
55
+ resolve: (data) => {
56
+ if (data.subject === RECOVER_CODE_DIALOG) {
57
+ state.recoveryCode = data.options?.props?.code;
58
+ }
59
+ },
60
+ }),
61
+ ),
62
+ ],
63
+ }),
64
+ withTheme,
65
+ withLayout({ tooltips: true }),
66
+ ],
67
+ parameters: {
68
+ layout: 'fullscreen',
69
+ translations,
70
+ },
71
+ };
72
+
73
+ export default meta;
74
+
75
+ type Story = StoryObj<typeof RecoverySetupDialog>;
76
+
77
+ export const Default: Story = {};