@dxos/plugin-client 0.9.0 → 0.9.1-main.c7dcc2e112

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 (190) hide show
  1. package/dist/lib/neutral/{AccountContainer-FZ3IC6H4.mjs → AccountContainer-LIMGLO2Y.mjs} +3 -3
  2. package/dist/lib/neutral/AccountContainer-LIMGLO2Y.mjs.map +7 -0
  3. package/dist/lib/neutral/ClientPlugin.node.mjs +477 -370
  4. package/dist/lib/neutral/ClientPlugin.node.mjs.map +4 -4
  5. package/dist/lib/neutral/{DevicesContainer-6MWNUC3I.mjs → DevicesContainer-X26OGLFK.mjs} +12 -12
  6. package/dist/lib/neutral/DevicesContainer-X26OGLFK.mjs.map +7 -0
  7. package/dist/lib/neutral/{InvitationsContainer-ME722VKS.mjs → InvitationsContainer-OBBCJ7CJ.mjs} +2 -2
  8. package/dist/lib/neutral/InvitationsContainer-OBBCJ7CJ.mjs.map +7 -0
  9. package/dist/lib/neutral/{JoinDialog-2NXAKPLP.mjs → JoinDialog-4BWV4CYB.mjs} +2 -2
  10. package/dist/lib/neutral/JoinDialog-4BWV4CYB.mjs.map +7 -0
  11. package/dist/lib/neutral/{ProfileContainer-VPX3FPWG.mjs → ProfileContainer-FGS2MVWA.mjs} +2 -2
  12. package/dist/lib/neutral/ProfileContainer-FGS2MVWA.mjs.map +7 -0
  13. package/dist/lib/neutral/{RecoveryCodeDialog-EWHB6U5T.mjs → RecoveryCodeDialog-7EPNKCLB.mjs} +2 -2
  14. package/dist/lib/neutral/RecoveryCodeDialog-7EPNKCLB.mjs.map +7 -0
  15. package/dist/lib/neutral/{RecoveryCredentialsContainer-IPBGDMIA.mjs → RecoveryCredentialsContainer-SL4LYHBE.mjs} +3 -3
  16. package/dist/lib/neutral/RecoveryCredentialsContainer-SL4LYHBE.mjs.map +7 -0
  17. package/dist/lib/neutral/UsageContainer-HBPMLIK7.mjs +226 -0
  18. package/dist/lib/neutral/UsageContainer-HBPMLIK7.mjs.map +7 -0
  19. package/dist/lib/neutral/{app-graph-builder-A74W7C36.mjs → app-graph-builder-FTXJNX37.mjs} +28 -14
  20. package/dist/lib/neutral/app-graph-builder-FTXJNX37.mjs.map +7 -0
  21. package/dist/lib/neutral/capabilities/index.mjs +5 -5
  22. package/dist/lib/neutral/capabilities/node.mjs +3 -3
  23. package/dist/lib/neutral/chunk-54MDDWCV.mjs +8 -0
  24. package/dist/lib/neutral/{chunk-NTVDGDQ3.mjs.map → chunk-54MDDWCV.mjs.map} +2 -2
  25. package/dist/lib/neutral/chunk-6G4KZLBG.mjs +28 -0
  26. package/dist/lib/neutral/chunk-6G4KZLBG.mjs.map +7 -0
  27. package/dist/lib/neutral/{chunk-D6HPCLTS.mjs → chunk-7DEIKHB7.mjs} +16 -2
  28. package/dist/lib/neutral/chunk-7DEIKHB7.mjs.map +7 -0
  29. package/dist/lib/neutral/{chunk-TUK3ELUU.mjs → chunk-DA3LZQSD.mjs} +22 -21
  30. package/dist/lib/neutral/chunk-DA3LZQSD.mjs.map +7 -0
  31. package/dist/lib/neutral/chunk-GSKMAS3S.mjs +13 -0
  32. package/dist/lib/neutral/chunk-GSKMAS3S.mjs.map +7 -0
  33. package/dist/lib/neutral/containers/index.mjs +10 -8
  34. package/dist/lib/neutral/containers/index.mjs.map +3 -3
  35. package/dist/lib/neutral/{create-agent-6HLW4D3A.mjs → create-agent-U567RM7Z.mjs} +3 -3
  36. package/dist/lib/neutral/{create-identity-JIWAR5GW.mjs → create-identity-IKN2FJ5B.mjs} +3 -3
  37. package/dist/lib/neutral/{create-passkey-FOXEMXGF.mjs → create-passkey-JAD3ACY7.mjs} +7 -7
  38. package/dist/lib/neutral/create-passkey-JAD3ACY7.mjs.map +7 -0
  39. package/dist/lib/neutral/{create-recovery-code-ZWDKLPH4.mjs → create-recovery-code-JZYXR4BY.mjs} +4 -4
  40. package/dist/lib/neutral/index.mjs +4 -4
  41. package/dist/lib/neutral/{join-identity-QEQFAE2S.mjs → join-identity-PJCHDNLH.mjs} +3 -3
  42. package/dist/lib/neutral/{layer-specs-CQLXQBQJ.mjs → layer-specs-VQCJLV33.mjs} +2 -2
  43. package/dist/lib/neutral/{layer-specs-CQLXQBQJ.mjs.map → layer-specs-VQCJLV33.mjs.map} +2 -2
  44. package/dist/lib/neutral/meta.json +1 -1
  45. package/dist/lib/neutral/meta.mjs +1 -1
  46. package/dist/lib/neutral/{navigation-handler-LU26CDFR.mjs → navigation-handler-MPJAVHWY.mjs} +3 -3
  47. package/dist/lib/neutral/open-usage-24I7RAFL.mjs +27 -0
  48. package/dist/lib/neutral/open-usage-24I7RAFL.mjs.map +7 -0
  49. package/dist/lib/neutral/operations/index.mjs +2 -2
  50. package/dist/lib/neutral/plugin.mjs +3 -3
  51. package/dist/lib/neutral/{react-context-B5LXMHVM.mjs → react-context-V74T6JOJ.mjs} +2 -2
  52. package/dist/lib/neutral/{react-context-B5LXMHVM.mjs.map → react-context-V74T6JOJ.mjs.map} +3 -3
  53. package/dist/lib/neutral/{react-surface-DEKGWTCD.mjs → react-surface-HPXRA65U.mjs} +8 -3
  54. package/dist/lib/neutral/react-surface-HPXRA65U.mjs.map +7 -0
  55. package/dist/lib/neutral/{recover-identity-JNFKLCUD.mjs → recover-identity-B7YM2BR4.mjs} +3 -3
  56. package/dist/lib/neutral/{redeem-passkey-4SBAI4AG.mjs → redeem-passkey-4DR7GPMU.mjs} +10 -10
  57. package/dist/lib/neutral/redeem-passkey-4DR7GPMU.mjs.map +7 -0
  58. package/dist/lib/neutral/{redeem-token-POBD6TMN.mjs → redeem-token-N5MBQG3I.mjs} +3 -3
  59. package/dist/lib/neutral/{reset-storage-LQKUV5RA.mjs → reset-storage-K47HZTGP.mjs} +3 -3
  60. package/dist/lib/neutral/{share-identity-KNQ2XNXV.mjs → share-identity-YJIFDIPJ.mjs} +6 -6
  61. package/dist/lib/neutral/share-identity-YJIFDIPJ.mjs.map +7 -0
  62. package/dist/lib/neutral/testing/index.mjs +2 -2
  63. package/dist/lib/neutral/testing/index.mjs.map +3 -3
  64. package/dist/lib/neutral/translations.mjs +24 -1
  65. package/dist/lib/neutral/translations.mjs.map +3 -3
  66. package/dist/lib/neutral/types/index.mjs +1 -1
  67. package/dist/types/dx.config.d.ts +28 -0
  68. package/dist/types/dx.config.d.ts.map +1 -0
  69. package/dist/types/src/capabilities/index.d.ts +9 -43
  70. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  71. package/dist/types/src/capabilities/navigation-handler/index.d.ts +1 -1
  72. package/dist/types/src/capabilities/navigation-handler/index.d.ts.map +1 -1
  73. package/dist/types/src/capabilities/node.d.ts +6 -6
  74. package/dist/types/src/capabilities/node.d.ts.map +1 -1
  75. package/dist/types/src/capabilities/react-surface.d.ts +2 -2
  76. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  77. package/dist/types/src/commands/account/index.d.ts +3 -0
  78. package/dist/types/src/commands/account/index.d.ts.map +1 -0
  79. package/dist/types/src/commands/account/login/index.d.ts +2 -0
  80. package/dist/types/src/commands/account/login/index.d.ts.map +1 -0
  81. package/dist/types/src/commands/account/login/login.d.ts +12 -0
  82. package/dist/types/src/commands/account/login/login.d.ts.map +1 -0
  83. package/dist/types/src/commands/account/logout/index.d.ts +2 -0
  84. package/dist/types/src/commands/account/logout/index.d.ts.map +1 -0
  85. package/dist/types/src/commands/account/logout/logout.d.ts +9 -0
  86. package/dist/types/src/commands/account/logout/logout.d.ts.map +1 -0
  87. package/dist/types/src/commands/halo/join/join.d.ts +1 -1
  88. package/dist/types/src/commands/halo/join/join.d.ts.map +1 -1
  89. package/dist/types/src/commands/halo/util.d.ts +4 -0
  90. package/dist/types/src/commands/halo/util.d.ts.map +1 -1
  91. package/dist/types/src/commands/index.d.ts +1 -0
  92. package/dist/types/src/commands/index.d.ts.map +1 -1
  93. package/dist/types/src/constants.d.ts.map +1 -1
  94. package/dist/types/src/containers/DevicesContainer/DevicesContainer.d.ts.map +1 -1
  95. package/dist/types/src/containers/RecoveryCredentialsContainer/RecoveryCredentialsContainer.d.ts.map +1 -1
  96. package/dist/types/src/containers/UsageContainer/UsageContainer.d.ts +7 -0
  97. package/dist/types/src/containers/UsageContainer/UsageContainer.d.ts.map +1 -0
  98. package/dist/types/src/containers/UsageContainer/UsageView.d.ts +19 -0
  99. package/dist/types/src/containers/UsageContainer/UsageView.d.ts.map +1 -0
  100. package/dist/types/src/containers/UsageContainer/UsageView.stories.d.ts +19 -0
  101. package/dist/types/src/containers/UsageContainer/UsageView.stories.d.ts.map +1 -0
  102. package/dist/types/src/containers/UsageContainer/index.d.ts +2 -0
  103. package/dist/types/src/containers/UsageContainer/index.d.ts.map +1 -0
  104. package/dist/types/src/containers/index.d.ts +1 -0
  105. package/dist/types/src/containers/index.d.ts.map +1 -1
  106. package/dist/types/src/meta.d.ts +28 -2
  107. package/dist/types/src/meta.d.ts.map +1 -1
  108. package/dist/types/src/operations/create-passkey.d.ts.map +1 -1
  109. package/dist/types/src/operations/definitions.d.ts +1 -0
  110. package/dist/types/src/operations/definitions.d.ts.map +1 -1
  111. package/dist/types/src/operations/index.d.ts.map +1 -1
  112. package/dist/types/src/operations/open-usage.d.ts +5 -0
  113. package/dist/types/src/operations/open-usage.d.ts.map +1 -0
  114. package/dist/types/src/testing/initializeIdentity.d.ts.map +1 -1
  115. package/dist/types/src/translations.d.ts.map +1 -1
  116. package/dist/types/src/types/capabilities.d.ts.map +1 -1
  117. package/dist/types/src/types/events.d.ts.map +1 -1
  118. package/dist/types/src/types/schema.d.ts +1 -0
  119. package/dist/types/src/types/schema.d.ts.map +1 -1
  120. package/dist/types/tsconfig.tsbuildinfo +1 -1
  121. package/dx.config.ts +19 -0
  122. package/package.json +37 -35
  123. package/src/ClientPlugin.node.ts +2 -2
  124. package/src/ClientPlugin.test.ts +1 -1
  125. package/src/capabilities/app-graph-builder.ts +22 -13
  126. package/src/capabilities/layer-specs.ts +1 -1
  127. package/src/capabilities/react-context.tsx +1 -1
  128. package/src/capabilities/react-surface.tsx +6 -0
  129. package/src/commands/account/index.ts +13 -0
  130. package/src/commands/account/login/index.ts +5 -0
  131. package/src/commands/account/login/login.ts +147 -0
  132. package/src/commands/account/logout/index.ts +5 -0
  133. package/src/commands/account/logout/logout.ts +49 -0
  134. package/src/commands/halo/create/create.ts +2 -2
  135. package/src/commands/halo/index.ts +6 -6
  136. package/src/commands/halo/join/join.ts +2 -13
  137. package/src/commands/halo/util.ts +24 -0
  138. package/src/commands/index.ts +1 -0
  139. package/src/constants.ts +3 -3
  140. package/src/containers/AccountContainer/AccountContainer.tsx +1 -1
  141. package/src/containers/DevicesContainer/DevicesContainer.tsx +14 -11
  142. package/src/containers/InvitationsContainer/InvitationsContainer.tsx +1 -1
  143. package/src/containers/JoinDialog/JoinDialog.tsx +1 -1
  144. package/src/containers/ProfileContainer/ProfileContainer.tsx +1 -1
  145. package/src/containers/RecoveryCodeDialog/RecoveryCodeDialog.tsx +1 -1
  146. package/src/containers/RecoveryCredentialsContainer/RecoveryCredentialsContainer.tsx +2 -2
  147. package/src/containers/UsageContainer/UsageContainer.tsx +47 -0
  148. package/src/containers/UsageContainer/UsageView.stories.tsx +114 -0
  149. package/src/containers/UsageContainer/UsageView.tsx +238 -0
  150. package/src/containers/UsageContainer/index.ts +5 -0
  151. package/src/containers/index.ts +1 -0
  152. package/src/meta.ts +3 -12
  153. package/src/operations/create-passkey.ts +6 -4
  154. package/src/operations/definitions.ts +12 -1
  155. package/src/operations/index.ts +1 -0
  156. package/src/operations/open-usage.ts +24 -0
  157. package/src/operations/redeem-passkey.ts +7 -7
  158. package/src/operations/share-identity.ts +3 -3
  159. package/src/testing/initializeIdentity.ts +4 -2
  160. package/src/translations.ts +25 -1
  161. package/src/types/capabilities.ts +7 -5
  162. package/src/types/events.ts +5 -5
  163. package/src/types/schema.ts +23 -16
  164. package/dist/lib/neutral/AccountContainer-FZ3IC6H4.mjs.map +0 -7
  165. package/dist/lib/neutral/DevicesContainer-6MWNUC3I.mjs.map +0 -7
  166. package/dist/lib/neutral/InvitationsContainer-ME722VKS.mjs.map +0 -7
  167. package/dist/lib/neutral/JoinDialog-2NXAKPLP.mjs.map +0 -7
  168. package/dist/lib/neutral/ProfileContainer-VPX3FPWG.mjs.map +0 -7
  169. package/dist/lib/neutral/RecoveryCodeDialog-EWHB6U5T.mjs.map +0 -7
  170. package/dist/lib/neutral/RecoveryCredentialsContainer-IPBGDMIA.mjs.map +0 -7
  171. package/dist/lib/neutral/app-graph-builder-A74W7C36.mjs.map +0 -7
  172. package/dist/lib/neutral/chunk-D6HPCLTS.mjs.map +0 -7
  173. package/dist/lib/neutral/chunk-JOCCVODY.mjs +0 -21
  174. package/dist/lib/neutral/chunk-JOCCVODY.mjs.map +0 -7
  175. package/dist/lib/neutral/chunk-NTVDGDQ3.mjs +0 -8
  176. package/dist/lib/neutral/chunk-TUK3ELUU.mjs.map +0 -7
  177. package/dist/lib/neutral/chunk-WWCVZG3K.mjs +0 -13
  178. package/dist/lib/neutral/chunk-WWCVZG3K.mjs.map +0 -7
  179. package/dist/lib/neutral/create-passkey-FOXEMXGF.mjs.map +0 -7
  180. package/dist/lib/neutral/react-surface-DEKGWTCD.mjs.map +0 -7
  181. package/dist/lib/neutral/redeem-passkey-4SBAI4AG.mjs.map +0 -7
  182. package/dist/lib/neutral/share-identity-KNQ2XNXV.mjs.map +0 -7
  183. /package/dist/lib/neutral/{create-agent-6HLW4D3A.mjs.map → create-agent-U567RM7Z.mjs.map} +0 -0
  184. /package/dist/lib/neutral/{create-identity-JIWAR5GW.mjs.map → create-identity-IKN2FJ5B.mjs.map} +0 -0
  185. /package/dist/lib/neutral/{create-recovery-code-ZWDKLPH4.mjs.map → create-recovery-code-JZYXR4BY.mjs.map} +0 -0
  186. /package/dist/lib/neutral/{join-identity-QEQFAE2S.mjs.map → join-identity-PJCHDNLH.mjs.map} +0 -0
  187. /package/dist/lib/neutral/{navigation-handler-LU26CDFR.mjs.map → navigation-handler-MPJAVHWY.mjs.map} +0 -0
  188. /package/dist/lib/neutral/{recover-identity-JNFKLCUD.mjs.map → recover-identity-B7YM2BR4.mjs.map} +0 -0
  189. /package/dist/lib/neutral/{redeem-token-POBD6TMN.mjs.map → redeem-token-N5MBQG3I.mjs.map} +0 -0
  190. /package/dist/lib/neutral/{reset-storage-LQKUV5RA.mjs.map → reset-storage-K47HZTGP.mjs.map} +0 -0
@@ -25,7 +25,7 @@ export type DevicesContainerProps = {
25
25
  };
26
26
 
27
27
  export const DevicesContainer = ({ createInvitationUrl }: DevicesContainerProps) => {
28
- const { t } = useTranslation(meta.id);
28
+ const { t } = useTranslation(meta.profile.key);
29
29
  const { invokePromise } = useOperationInvoker();
30
30
  const devices = useDevices();
31
31
  const { swarm: connectionState } = useNetworkStatus();
@@ -46,12 +46,12 @@ export const DevicesContainer = ({ createInvitationUrl }: DevicesContainerProps)
46
46
  <Clipboard.Provider>
47
47
  <Settings.Viewport>
48
48
  <Settings.Section
49
- title={t('devices-verbose.label', { ns: meta.id })}
50
- description={t('devices.description', { ns: meta.id })}
49
+ title={t('devices-verbose.label', { ns: meta.profile.key })}
50
+ description={t('devices.description', { ns: meta.profile.key })}
51
51
  >
52
52
  <Settings.Panel>
53
53
  <div role='group' className='min-w-0'>
54
- <h3 className='text-lg mb-2'>{t('devices.label', { ns: meta.id })}</h3>
54
+ <h3 className='text-lg mb-2'>{t('devices.label', { ns: meta.profile.key })}</h3>
55
55
  <List>
56
56
  {devices.map((device: Device) => (
57
57
  <DeviceListItem key={device.deviceKey.toHex()} device={device} connectionState={connectionState} />
@@ -67,17 +67,20 @@ export const DevicesContainer = ({ createInvitationUrl }: DevicesContainerProps)
67
67
  </Settings.Panel>
68
68
  </Settings.Section>
69
69
  <Settings.Section
70
- title={t('danger-zone.title', { ns: meta.id })}
71
- description={t('danger-zone.description', { ns: meta.id })}
70
+ title={t('danger-zone.title', { ns: meta.profile.key })}
71
+ description={t('danger-zone.description', { ns: meta.profile.key })}
72
72
  >
73
- <Settings.Item title={t('reset-device.label')} description={t('reset-device.description', { ns: meta.id })}>
73
+ <Settings.Item
74
+ title={t('reset-device.label')}
75
+ description={t('reset-device.description', { ns: meta.profile.key })}
76
+ >
74
77
  <Button variant='destructive' onClick={handleResetStorage} data-testid='devicesContainer.reset'>
75
78
  {t('reset-device.label')}
76
79
  </Button>
77
80
  </Settings.Item>
78
81
  <Settings.Item
79
82
  title={t('recover-identity.label')}
80
- description={t('recover-identity.description', { ns: meta.id })}
83
+ description={t('recover-identity.description', { ns: meta.profile.key })}
81
84
  >
82
85
  <Button variant='destructive' onClick={handleRecover} data-testid='devicesContainer.recover'>
83
86
  {t('recover-identity.label')}
@@ -85,7 +88,7 @@ export const DevicesContainer = ({ createInvitationUrl }: DevicesContainerProps)
85
88
  </Settings.Item>
86
89
  <Settings.Item
87
90
  title={t('join-new-identity.label')}
88
- description={t('join-new-identity.description', { ns: meta.id })}
91
+ description={t('join-new-identity.description', { ns: meta.profile.key })}
89
92
  >
90
93
  <Button variant='destructive' onClick={handleJoinNewIdentity} data-testid='devicesContainer.joinExisting'>
91
94
  {t('join-new-identity.label')}
@@ -167,7 +170,7 @@ const InvitationSection = ({
167
170
  onInvitationDone = () => {},
168
171
  onInvitationCreate = () => {},
169
172
  }: InvitationComponentProps) => {
170
- const { t } = useTranslation(meta.id);
173
+ const { t } = useTranslation(meta.profile.key);
171
174
  const activeView =
172
175
  state < 0
173
176
  ? 'init'
@@ -215,7 +218,7 @@ const InvitationQR = ({ id, url, onCancel }: { id: string; url: string; onCancel
215
218
  const emoji = hexToEmoji(id);
216
219
  return (
217
220
  <>
218
- <p className='text-description'>{t('qr-code.description', { ns: meta.id })}</p>
221
+ <p className='text-description'>{t('qr-code.description', { ns: meta.profile.key })}</p>
219
222
  <div role='group' className='grid grid-cols-[1fr_min-content]'>
220
223
  <div className='flex justify-center py-4'>
221
224
  <div className='w-full md:max-w-80 aspect-square relative text-description'>
@@ -17,7 +17,7 @@ import { type AccountCacheInvitation } from '../../state/account-cache';
17
17
  import { useHubHttpClient } from '../../state/use-hub-http';
18
18
 
19
19
  export const InvitationsContainer = () => {
20
- const { t } = useTranslation(meta.id);
20
+ const { t } = useTranslation(meta.profile.key);
21
21
  const accountCacheAtom = useCapability(ClientCapabilities.AccountCache);
22
22
  const [cache] = useAtom(accountCacheAtom);
23
23
  const setCache = useAtomSet(accountCacheAtom);
@@ -17,7 +17,7 @@ import { ClientOperation } from '#operations';
17
17
 
18
18
  export const JoinDialog = (props: JoinPanelProps) => {
19
19
  const { invokePromise } = useOperationInvoker();
20
- const { t } = useTranslation(meta.id);
20
+ const { t } = useTranslation(meta.profile.key);
21
21
 
22
22
  const handleCancelResetStorage = useCallback(() => invokePromise(ClientOperation.ShareIdentity), [invokePromise]);
23
23
 
@@ -33,7 +33,7 @@ const getDefaultEmojiValue = (identity: Identity | null) => hexToEmoji(identity?
33
33
  const getEmojiValue = (identity: Identity | null) => identity?.profile?.data?.emoji || getDefaultEmojiValue(identity);
34
34
 
35
35
  export const ProfileContainer = () => {
36
- const { t } = useTranslation(meta.id);
36
+ const { t } = useTranslation(meta.profile.key);
37
37
  const client = useClient();
38
38
  const identity = useIdentity();
39
39
  const [displayName, setDisplayNameDirectly] = useState(identity?.profile?.displayName ?? '');
@@ -14,7 +14,7 @@ export type RecoveryCodeDialogProps = {
14
14
 
15
15
  // TODO(burdon): Should have cancel button.
16
16
  export const RecoveryCodeDialog = ({ code }: RecoveryCodeDialogProps) => {
17
- const { t } = useTranslation(meta.id);
17
+ const { t } = useTranslation(meta.profile.key);
18
18
  const [confirmation, setConfirmation] = useState(false);
19
19
 
20
20
  const handleConfirmation = useCallback((checked: boolean) => setConfirmation(checked), []);
@@ -12,10 +12,10 @@ import { Settings } from '@dxos/react-ui-form';
12
12
  import { meta } from '#meta';
13
13
  import { ClientOperation } from '#operations';
14
14
 
15
- export const MANAGE_CREDENTIALS_DIALOG = `${meta.id}.ManageCredentialsDialog`;
15
+ export const MANAGE_CREDENTIALS_DIALOG = `${meta.profile.key}.ManageCredentialsDialog`;
16
16
 
17
17
  export const RecoveryCredentialsContainer = () => {
18
- const { t } = useTranslation(meta.id);
18
+ const { t } = useTranslation(meta.profile.key);
19
19
  const { invokePromise } = useOperationInvoker();
20
20
  const credentials = useCredentials();
21
21
  const recoveryCredentials = credentials.filter(
@@ -0,0 +1,47 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useState } from 'react';
6
+
7
+ import { Context } from '@dxos/context';
8
+ import { log } from '@dxos/log';
9
+ import { type GetProfileUsageResponse } from '@dxos/protocols';
10
+ import { useAsyncEffect } from '@dxos/react-ui';
11
+
12
+ import { useHubHttpClient } from '../../state/use-hub-http';
13
+ import { UsageView, type UsageViewState } from './UsageView';
14
+
15
+ /**
16
+ * Fetches rolling-window profile usage from the hub service and renders it.
17
+ * Connected wrapper around the presentational {@link UsageView}.
18
+ */
19
+ export const UsageContainer = () => {
20
+ const hubHttp = useHubHttpClient();
21
+ const [state, setState] = useState<UsageViewState>('loading');
22
+ const [data, setData] = useState<GetProfileUsageResponse | undefined>();
23
+ const [lastUpdated, setLastUpdated] = useState<number | undefined>();
24
+ const [refreshCount, setRefreshCount] = useState(0);
25
+
26
+ useAsyncEffect(async () => {
27
+ if (!hubHttp) {
28
+ setState('unavailable');
29
+ return;
30
+ }
31
+ // Keep prior data visible while refetching; only show the skeleton on first load.
32
+ setState((previous) => (previous === 'ready' ? previous : 'loading'));
33
+ try {
34
+ const result = await hubHttp.getProfileUsage(new Context());
35
+ setData(result);
36
+ setLastUpdated(Date.now());
37
+ setState('ready');
38
+ } catch (err) {
39
+ log.catch(err);
40
+ setState('error');
41
+ }
42
+ }, [hubHttp, refreshCount]);
43
+
44
+ const handleRefresh = useCallback(() => setRefreshCount((count) => count + 1), []);
45
+
46
+ return <UsageView state={state} data={data} lastUpdated={lastUpdated} onRefresh={handleRefresh} />;
47
+ };
@@ -0,0 +1,114 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+
7
+ import { type GetProfileUsageResponse } from '@dxos/protocols';
8
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
9
+
10
+ import { translations } from '#translations';
11
+
12
+ import { UsageView } from './UsageView';
13
+
14
+ // Mock payload shaped like the `/api/metering/profile/usage` response.
15
+ const usageData: GetProfileUsageResponse = {
16
+ profileId: 'did:key:z6MkExampleProfileIdentity',
17
+ usage: [
18
+ { eventType: 'ai', valueKey: 'inputTokens', subtypePattern: ['claude-opus-4'], amount: 1_284_500 },
19
+ { eventType: 'ai', valueKey: 'outputTokens', subtypePattern: ['claude-opus-4'], amount: 238_900 },
20
+ { eventType: 'ai', valueKey: 'inputTokens', subtypePattern: ['claude-sonnet-4'], amount: 92_300 },
21
+ { eventType: 'ai', valueKey: 'outputTokens', subtypePattern: ['claude-sonnet-4'], amount: 18_400 },
22
+ ],
23
+ limits: [
24
+ {
25
+ eventType: 'ai',
26
+ valueKey: 'inputTokens',
27
+ subtypePattern: ['claude-opus-4'],
28
+ limit: 5_000_000,
29
+ windowDuration: 24 * 7 * 60 * 60,
30
+ },
31
+ {
32
+ eventType: 'ai',
33
+ valueKey: 'outputTokens',
34
+ subtypePattern: ['*'],
35
+ limit: 1_000_000,
36
+ windowDuration: 24 * 30 * 60 * 60,
37
+ },
38
+ {
39
+ eventType: 'ai',
40
+ valueKey: 'inputTokens',
41
+ subtypePattern: ['*'],
42
+ limit: 20_000_000,
43
+ windowDuration: 24 * 30 * 60 * 60,
44
+ },
45
+ ],
46
+ buckets: [],
47
+ };
48
+
49
+ const meta = {
50
+ title: 'plugins/plugin-client/containers/UsageView',
51
+ component: UsageView,
52
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
53
+ parameters: {
54
+ layout: 'fullscreen',
55
+ translations,
56
+ },
57
+ } satisfies Meta<typeof UsageView>;
58
+
59
+ export default meta;
60
+
61
+ type Story = StoryObj<typeof meta>;
62
+
63
+ export const Default: Story = {
64
+ args: {
65
+ state: 'ready',
66
+ data: usageData,
67
+ lastUpdated: new Date('2026-06-16T12:00:00Z').getTime(),
68
+ onRefresh: () => {},
69
+ },
70
+ };
71
+
72
+ // Internal plan: a single unlimited (`limit: null`) `ai` limit — renders without a meter.
73
+ export const Internal: Story = {
74
+ args: {
75
+ state: 'ready',
76
+ data: {
77
+ profileId: usageData.profileId,
78
+ usage: usageData.usage,
79
+ limits: [
80
+ {
81
+ eventType: 'ai',
82
+ valueKey: 'outputTokens',
83
+ subtypePattern: ['*'],
84
+ limit: null,
85
+ windowDuration: 24 * 30 * 60 * 60,
86
+ },
87
+ ],
88
+ buckets: [],
89
+ },
90
+ lastUpdated: new Date('2026-06-16T12:00:00Z').getTime(),
91
+ onRefresh: () => {},
92
+ },
93
+ };
94
+
95
+ export const Loading: Story = {
96
+ args: { state: 'loading' },
97
+ };
98
+
99
+ export const Error: Story = {
100
+ args: { state: 'error' },
101
+ };
102
+
103
+ export const Empty: Story = {
104
+ args: {
105
+ state: 'ready',
106
+ data: { profileId: usageData.profileId, usage: [], limits: [], buckets: [] },
107
+ lastUpdated: new Date('2026-06-16T12:00:00Z').getTime(),
108
+ onRefresh: () => {},
109
+ },
110
+ };
111
+
112
+ export const Unavailable: Story = {
113
+ args: { state: 'unavailable' },
114
+ };
@@ -0,0 +1,238 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React, { useMemo, useState } from 'react';
6
+
7
+ import { type GetProfileUsageResponse, type MeteringLimit, type MeteringUsageItem } from '@dxos/protocols';
8
+ import { IconButton, Message, useTranslation } from '@dxos/react-ui';
9
+ import { Settings } from '@dxos/react-ui-form';
10
+ import { JsonHighlighter } from '@dxos/react-ui-syntax-highlighter';
11
+
12
+ import { meta } from '#meta';
13
+
14
+ export type UsageViewState = 'loading' | 'ready' | 'error' | 'unavailable';
15
+
16
+ export type UsageViewProps = {
17
+ /** Current fetch state; drives which content is shown. */
18
+ state: UsageViewState;
19
+ /** Profile usage payload; required when `state` is `'ready'`. */
20
+ data?: GetProfileUsageResponse;
21
+ /** Epoch milliseconds of the last successful fetch. */
22
+ lastUpdated?: number;
23
+ /** Invoked when the user requests a refresh. */
24
+ onRefresh?: () => void;
25
+ };
26
+
27
+ type TFunction = (key: string, options?: Record<string, unknown>) => string;
28
+
29
+ /**
30
+ * Whether a usage row falls under a limit. Subtype segments are matched positionally against
31
+ * `subtypePattern` where `*` matches any segment.
32
+ */
33
+ const usageMatchesLimit = (item: MeteringUsageItem, limit: MeteringLimit): boolean => {
34
+ if (item.eventType !== limit.eventType || item.valueKey !== limit.valueKey) {
35
+ return false;
36
+ }
37
+ return limit.subtypePattern.every((pattern, index) => pattern === '*' || pattern === item.subtypePattern[index]);
38
+ };
39
+
40
+ /** Sum every usage item that falls under the limit. */
41
+ const sumUsageForLimit = (usage: GetProfileUsageResponse['usage'], limit: MeteringLimit): number =>
42
+ usage.reduce((total, item) => (usageMatchesLimit(item, limit) ? total + item.amount : total), 0);
43
+
44
+ /** Humanize a camelCase value key, e.g. `outputTokens` → `output tokens`. */
45
+ const formatValueKey = (valueKey: string): string =>
46
+ valueKey
47
+ .replace(/([A-Z])/g, ' $1')
48
+ .trim()
49
+ .toLowerCase();
50
+
51
+ /**
52
+ * Human label for a limit: a scope followed by the value key, e.g.
53
+ * `{ eventType: 'ai', valueKey: 'outputTokens', subtypePattern: ['*'] }` → `All models · output tokens`,
54
+ * `{ eventType: 'ai', valueKey: 'inputTokens', subtypePattern: ['claude-opus-4'] }` → `claude-opus-4 · input tokens`.
55
+ */
56
+ const formatLimitLabel = (limit: MeteringLimit, t: TFunction): string => {
57
+ const metric = formatValueKey(limit.valueKey);
58
+ const subtype = limit.subtypePattern.filter((segment) => segment && segment !== '*').join('/');
59
+ if (limit.eventType === 'ai') {
60
+ return `${subtype || t('usage-all-models.label')} · ${metric}`;
61
+ }
62
+ return `${subtype ? `${limit.eventType}/${subtype}` : limit.eventType} · ${metric}`;
63
+ };
64
+
65
+ const formatAmount = (amount: number): string => amount.toLocaleString();
66
+
67
+ const SECONDS_PER_HOUR = 60 * 60;
68
+
69
+ /** Render `windowHours` as a localized "N months / days / hours" string. */
70
+ const formatWindow = (windowHours: number, t: TFunction): string => {
71
+ if (windowHours % (24 * 30) === 0) {
72
+ return t('usage-window-months.label', { count: windowHours / (24 * 30) });
73
+ }
74
+ if (windowHours % 24 === 0) {
75
+ return t('usage-window-days.label', { count: windowHours / 24 });
76
+ }
77
+ return t('usage-window-hours.label', { count: windowHours });
78
+ };
79
+
80
+ /**
81
+ * A single usage row: label + caption on the left, a thin meter, and a value on the right.
82
+ * Omitting `percent` (an unlimited limit) renders no meter.
83
+ */
84
+ const UsageRow = ({
85
+ label,
86
+ caption,
87
+ percent,
88
+ value,
89
+ }: {
90
+ label: string;
91
+ caption?: string;
92
+ percent?: number;
93
+ value: string;
94
+ }) => (
95
+ <div className='flex items-center gap-trim-lg py-trim-sm'>
96
+ <div className='flex flex-col min-w-0 w-1/3 shrink-0'>
97
+ <span className='truncate text-base-fg'>{label}</span>
98
+ {caption && <span className='text-sm text-description'>{caption}</span>}
99
+ </div>
100
+ {percent === undefined ? (
101
+ <div className='flex-1' />
102
+ ) : (
103
+ <div
104
+ role='progressbar'
105
+ aria-label={label}
106
+ aria-valuenow={percent}
107
+ aria-valuemin={0}
108
+ aria-valuemax={100}
109
+ className='h-1.5 flex-1 rounded-full bg-separator overflow-hidden'
110
+ >
111
+ <div
112
+ className='h-full rounded-full bg-primary-500 transition-[width] duration-500'
113
+ style={{ width: `${percent}%` }}
114
+ />
115
+ </div>
116
+ )}
117
+ <span className='shrink-0 w-24 text-end text-sm text-description tabular-nums'>{value}</span>
118
+ </div>
119
+ );
120
+
121
+ /** Placeholder rows shown while the first fetch is in flight. */
122
+ const UsageSkeleton = () => (
123
+ <div className='flex flex-col'>
124
+ {Array.from({ length: 3 }).map((_, index) => (
125
+ <div key={index} className='flex items-center gap-trim-lg py-trim-sm'>
126
+ <div className='flex flex-col gap-1 w-1/3 shrink-0'>
127
+ <div className='h-4 w-24 rounded bg-separator animate-pulse' />
128
+ <div className='h-3 w-32 rounded bg-separator animate-pulse' />
129
+ </div>
130
+ <div className='h-1.5 flex-1 rounded-full bg-separator animate-pulse' />
131
+ <div className='h-3 w-12 shrink-0 rounded bg-separator animate-pulse' />
132
+ </div>
133
+ ))}
134
+ </div>
135
+ );
136
+
137
+ /**
138
+ * Presentational view of profile usage: renders loading/error/unavailable/empty states, or one row
139
+ * per limit with a usage meter, plus a collapsible raw-response viewer. Pure — all data via props.
140
+ */
141
+ export const UsageView = ({ state, data, lastUpdated, onRefresh }: UsageViewProps) => {
142
+ const { t } = useTranslation(meta.profile.key);
143
+ const [rawExpanded, setRawExpanded] = useState(false);
144
+
145
+ // One display row per limit, shorter windows first. A `null` amount means unlimited (rendered without a meter).
146
+ const limitRows = useMemo(() => {
147
+ if (!data) {
148
+ return [];
149
+ }
150
+ return data.limits
151
+ .map((limit) => {
152
+ const used = sumUsageForLimit(data.usage, limit);
153
+ const label = formatLimitLabel(limit, t);
154
+ const windowHours = limit.windowDuration / SECONDS_PER_HOUR;
155
+ const window = formatWindow(windowHours, t);
156
+ const key = `${limit.eventType}/${limit.subtypePattern.join(',')}/${limit.valueKey}/${limit.windowDuration}/${limit.limit ?? 'unlimited'}`;
157
+ if (limit.limit === null) {
158
+ return {
159
+ key,
160
+ label,
161
+ caption: t('usage-unlimited.description', { used: formatAmount(used), window }),
162
+ percent: undefined,
163
+ value: t('usage-unlimited.label'),
164
+ windowHours,
165
+ };
166
+ }
167
+ const percent = limit.limit > 0 ? Math.min(100, Math.round((used / limit.limit) * 100)) : used > 0 ? 100 : 0;
168
+ return {
169
+ key,
170
+ label,
171
+ caption: t('usage-limit.description', { used: formatAmount(used), limit: formatAmount(limit.limit), window }),
172
+ percent,
173
+ value: t('usage-percent-used.label', { percent }),
174
+ windowHours,
175
+ };
176
+ })
177
+ .sort((a, b) => a.windowHours - b.windowHours);
178
+ }, [data, t]);
179
+
180
+ return (
181
+ <Settings.Viewport>
182
+ <Settings.Section title={t('usage-section.title')} description={t('usage-section.description')}>
183
+ {state === 'unavailable' ? (
184
+ <Message.Root valence='warning'>
185
+ <Message.Title icon='ph--cloud-slash--duotone'>{t('usage-unavailable.title')}</Message.Title>
186
+ <Message.Content>{t('usage-unavailable.description')}</Message.Content>
187
+ </Message.Root>
188
+ ) : state === 'loading' ? (
189
+ <UsageSkeleton />
190
+ ) : state === 'error' ? (
191
+ <Message.Root valence='error'>
192
+ <Message.Title icon='ph--cloud-x--duotone'>{t('usage-offline.title')}</Message.Title>
193
+ <Message.Content>{t('usage-offline.description')}</Message.Content>
194
+ </Message.Root>
195
+ ) : limitRows.length === 0 ? (
196
+ <Message.Root valence='neutral'>
197
+ <Message.Title icon='ph--chart-bar--duotone'>{t('usage-empty.title')}</Message.Title>
198
+ <Message.Content>{t('usage-empty.description')}</Message.Content>
199
+ </Message.Root>
200
+ ) : (
201
+ <div className='flex flex-col'>
202
+ {limitRows.map((row) => (
203
+ <UsageRow key={row.key} label={row.label} caption={row.caption} percent={row.percent} value={row.value} />
204
+ ))}
205
+
206
+ <div className='flex items-center gap-1 pt-trim-md text-sm text-description'>
207
+ {lastUpdated !== undefined && (
208
+ <span>{t('usage-last-updated.label', { time: new Date(lastUpdated).toLocaleTimeString() })}</span>
209
+ )}
210
+ {onRefresh && (
211
+ <IconButton
212
+ iconOnly
213
+ variant='ghost'
214
+ icon='ph--arrow-clockwise--regular'
215
+ label={t('usage-refresh.label')}
216
+ onClick={onRefresh}
217
+ />
218
+ )}
219
+ </div>
220
+ </div>
221
+ )}
222
+ {state === 'ready' && data && (
223
+ <div className='flex flex-col pt-trim-md'>
224
+ <IconButton
225
+ variant='ghost'
226
+ icon='ph--caret-right--regular'
227
+ iconClassNames={rawExpanded ? 'transition-transform rotate-90' : 'transition-transform'}
228
+ label={t('usage-raw-json.label')}
229
+ onClick={() => setRawExpanded((value) => !value)}
230
+ classNames='self-start'
231
+ />
232
+ {rawExpanded && <JsonHighlighter data={data} classNames='mt-trim-sm text-xs overflow-auto' />}
233
+ </div>
234
+ )}
235
+ </Settings.Section>
236
+ </Settings.Viewport>
237
+ );
238
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export { UsageContainer as default } from './UsageContainer';
@@ -10,6 +10,7 @@ export { type ResetDialogProps } from './ResetDialog';
10
10
  export const AccountContainer: ComponentType<any> = lazy(() => import('./AccountContainer'));
11
11
  export const DevicesContainer: ComponentType<any> = lazy(() => import('./DevicesContainer'));
12
12
  export const InvitationsContainer: ComponentType<any> = lazy(() => import('./InvitationsContainer'));
13
+ export const UsageContainer: ComponentType<any> = lazy(() => import('./UsageContainer'));
13
14
  export const JoinDialog: ComponentType<any> = lazy(() => import('./JoinDialog'));
14
15
  export const ProfileContainer: ComponentType<any> = lazy(() => import('./ProfileContainer'));
15
16
  export const RecoveryCodeDialog: ComponentType<any> = lazy(() => import('./RecoveryCodeDialog'));
package/src/meta.ts CHANGED
@@ -3,16 +3,7 @@
3
3
  //
4
4
 
5
5
  import { Plugin } from '@dxos/app-framework';
6
- import { DXN } from '@dxos/keys';
7
- import { trim } from '@dxos/util';
8
6
 
9
- export const meta = Plugin.makeMeta({
10
- key: DXN.make('org.dxos.plugin.client'),
11
- name: 'Client',
12
- author: 'DXOS',
13
- description: trim`
14
- Core client connectivity and peer-to-peer networking infrastructure.
15
- Manages identity, authentication, and real-time synchronization across devices.
16
- `,
17
- tags: ['system'],
18
- });
7
+ import config from '../dx.config';
8
+
9
+ export const meta = Plugin.getMetaFromConfig(config);
@@ -6,7 +6,7 @@ import * as Effect from 'effect/Effect';
6
6
  import * as Match from 'effect/Match';
7
7
 
8
8
  import { Capability } from '@dxos/app-framework';
9
- import { supportsNativePasskeys, createNativePasskey, extractPublicKeyFromAttestation } from '@dxos/app-toolkit';
9
+ import { NativePasskey } from '@dxos/app-toolkit';
10
10
  import { PublicKey } from '@dxos/client';
11
11
  import { Operation } from '@dxos/compute';
12
12
  import { invariant } from '@dxos/invariant';
@@ -23,16 +23,18 @@ const handler: Operation.WithHandler<typeof CreatePasskey> = CreatePasskey.pipe(
23
23
 
24
24
  const lookupKey = PublicKey.random();
25
25
 
26
- const { recoveryKey, algorithm } = yield* Match.value(supportsNativePasskeys()).pipe(
26
+ const { recoveryKey, algorithm } = yield* Match.value(NativePasskey.supportsNativePasskeys()).pipe(
27
27
  Match.when(true, () =>
28
28
  Effect.gen(function* () {
29
29
  const result = yield* Effect.promise(() =>
30
- createNativePasskey({
30
+ NativePasskey.createNativePasskey({
31
31
  username: identity.did,
32
32
  userId: lookupKey.asUint8Array(),
33
33
  }),
34
34
  );
35
- const { publicKey, algorithm: alg } = extractPublicKeyFromAttestation(result.attestation_object);
35
+ const { publicKey, algorithm: alg } = NativePasskey.extractPublicKeyFromAttestation(
36
+ result.attestation_object,
37
+ );
36
38
  return {
37
39
  recoveryKey: PublicKey.from(publicKey),
38
40
  algorithm: alg === -7 ? 'ES256' : 'ED25519',
@@ -10,7 +10,7 @@ import { DXN, IdentityDid, SpaceId } from '@dxos/keys';
10
10
 
11
11
  import { meta } from '#meta';
12
12
 
13
- const makeKey = (name: string) => DXN.make(`${meta.id}.operation.${name}`);
13
+ const makeKey = (name: string) => DXN.make(`${meta.profile.key}.operation.${name}`);
14
14
 
15
15
  const IdentitySchema = Schema.Struct({
16
16
  identityDid: IdentityDid,
@@ -57,6 +57,17 @@ export const ShareIdentity = Operation.make({
57
57
  output: Schema.Void,
58
58
  });
59
59
 
60
+ export const OpenUsage = Operation.make({
61
+ meta: {
62
+ key: makeKey('openUsage'),
63
+ name: 'Open Usage',
64
+ icon: 'ph--chart-bar--regular',
65
+ },
66
+ services: [Capability.Service],
67
+ input: Schema.Void,
68
+ output: Schema.Void,
69
+ });
70
+
60
71
  export const RecoverIdentity = Operation.make({
61
72
  meta: { key: makeKey('recoverIdentity'), name: 'Recover Identity', icon: 'ph--key--regular' },
62
73
  services: [Capability.Service],
@@ -12,6 +12,7 @@ export const ClientOperationHandlerSet = OperationHandlerSet.lazy(
12
12
  () => import('./create-passkey'),
13
13
  () => import('./create-recovery-code'),
14
14
  () => import('./join-identity'),
15
+ () => import('./open-usage'),
15
16
  () => import('./recover-identity'),
16
17
  () => import('./redeem-passkey'),
17
18
  () => import('./redeem-token'),
@@ -0,0 +1,24 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { LayoutOperation, Paths } from '@dxos/app-toolkit';
8
+ import { Operation } from '@dxos/compute';
9
+
10
+ import { Account } from '../types';
11
+ import { OpenUsage } from './definitions';
12
+
13
+ const handler: Operation.WithHandler<typeof OpenUsage> = OpenUsage.pipe(
14
+ Operation.withHandler(
15
+ Effect.fnUntraced(function* () {
16
+ yield* Operation.invoke(LayoutOperation.SwitchWorkspace, { subject: Paths.getSpacePath(Account.id) });
17
+ yield* Operation.invoke(LayoutOperation.Open, {
18
+ subject: [Paths.getSpacePath(Account.id, Account.Usage)],
19
+ });
20
+ }),
21
+ ),
22
+ );
23
+
24
+ export default handler;