@dxos/halo 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API_AUDIT.md +396 -0
- package/CONSUMER_MIGRATION.md +343 -0
- package/LICENSE +105 -0
- package/MIGRATION.md +254 -0
- package/MIGRATION_PLAN.md +217 -0
- package/README.md +25 -0
- package/SPEC.mdl +137 -0
- package/dist/lib/neutral/index.mjs +186 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/types/src/Identity.d.ts +166 -0
- package/dist/types/src/Identity.d.ts.map +1 -0
- package/dist/types/src/Invitation.d.ts +87 -0
- package/dist/types/src/Invitation.d.ts.map +1 -0
- package/dist/types/src/Space.d.ts +158 -0
- package/dist/types/src/Space.d.ts.map +1 -0
- package/dist/types/src/errors.d.ts +228 -0
- package/dist/types/src/errors.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +5 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/docs/halo.drawio.svg +119 -0
- package/moon.yml +11 -0
- package/package.json +31 -0
- package/src/Identity.ts +195 -0
- package/src/Invitation.ts +95 -0
- package/src/Space.ts +192 -0
- package/src/errors.ts +41 -0
- package/src/index.ts +9 -0
- package/tsconfig.json +19 -0
package/API_AUDIT.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# HALO API Audit — Composer & Plugins
|
|
2
|
+
|
|
3
|
+
Inventory of every HALO-related API from `@dxos/client` / `@dxos/react-client` consumed by
|
|
4
|
+
Composer (`packages/apps/composer-app`) and the plugins (`packages/plugins/*`), followed by an
|
|
5
|
+
evaluation of how `@dxos/halo` Effect services can replace that surface so application code no
|
|
6
|
+
longer depends on `@dxos/client` directly.
|
|
7
|
+
|
|
8
|
+
Companion to [MIGRATION.md](./MIGRATION.md) (HALO → Keyhive). The migration plan describes the
|
|
9
|
+
_backend_ rewrite; this document describes the _consumer-facing_ seam that must exist regardless
|
|
10
|
+
of which backend (legacy client, EDGE shim, or Keyhive) is active — see MIGRATION.md §A.2.
|
|
11
|
+
|
|
12
|
+
Method: static audit (2026-07-13) of `packages/apps/composer-app` and `packages/plugins`,
|
|
13
|
+
excluding tests. Storybook fixtures are included but flagged, since they define the testing
|
|
14
|
+
surface the new package must also serve.
|
|
15
|
+
|
|
16
|
+
## 1. Entry points
|
|
17
|
+
|
|
18
|
+
Import counts across Composer + plugins (production code and stories):
|
|
19
|
+
|
|
20
|
+
| Import source | Files | Notes |
|
|
21
|
+
| -------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
22
|
+
| `@dxos/react-client/echo` | 268 | Mostly ECHO db/query hooks (out of scope), but also `useSpaces`, `useSpace`, `useMembers`, `useSpaceInvitations`, `SpaceMember`, `SpaceState` |
|
|
23
|
+
| `@dxos/client` | 88 | `Client` type, `ClientService`, `PublicKey` re-export |
|
|
24
|
+
| `@dxos/react-client` | 64 | `useClient`, `useConfig`, `useMulticastObservable` |
|
|
25
|
+
| `@dxos/client/echo` | 52 | `getSpace`, `Space`, `SpaceState`, `SpaceMember` |
|
|
26
|
+
| `@dxos/react-client/halo` | 26 | `useIdentity`, `useDevices`, `useCredentials`, `Identity`, `Device`, `Credential` types |
|
|
27
|
+
| `@dxos/client/invitations` | 7 | `Invitation`, `InvitationEncoder`, `hostInvitation`, observables |
|
|
28
|
+
| `@dxos/react-client/invitations` | 4 | Same, from React entry point |
|
|
29
|
+
| `@dxos/client/halo` | 4 | `Identity`, `Device`, `DeviceKind`, `DeviceType`, `Credential` |
|
|
30
|
+
| `@dxos/client/edge` | 5 | `createEdgeIdentity` (credential-presentation auth) |
|
|
31
|
+
| `@dxos/shell/react` | 2+ | `SpaceMemberList`, `InvitationList`, `AuthCode` UI components |
|
|
32
|
+
|
|
33
|
+
Almost all plugins obtain the client via the `ClientCapabilities.Client` capability provided by
|
|
34
|
+
`plugin-client` — the capability system is already the single injection point, which makes the
|
|
35
|
+
service swap tractable.
|
|
36
|
+
|
|
37
|
+
## 2. API surface by aspect
|
|
38
|
+
|
|
39
|
+
### 2.1 Identity
|
|
40
|
+
|
|
41
|
+
`client.halo.identity` (a `MulticastObservable<Identity | null>`) is by far the most used API
|
|
42
|
+
(~40 call sites + 20 hook usages).
|
|
43
|
+
|
|
44
|
+
| API | Count | Consumers |
|
|
45
|
+
| ------------------------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
46
|
+
| `halo.identity.get()` | ~40 | Gating ("has identity?"): plugin-space (`navigation-handler`, `operations/join`, `spaces-ready`), plugin-registry, plugin-onboarding, plugin-client CLI commands. Reading fields (`identityKey`, `did`, `spaceKey`, `profile`): composer-app recovery pages, plugin-script deploy, plugin-transcription, plugin-meeting, plugin-iroh-beacon |
|
|
47
|
+
| `halo.identity.subscribe()` | 5 | plugin-client (`capabilities/client.ts` — layout mode on login), plugin-calls (`call-manager`), plugin-onboarding (`onboarding-manager`), plugin-assistant (`edge-model-resolver`) |
|
|
48
|
+
| `CreateAtom.fromObservable(identity)` | 1 | plugin-client `app-graph-builder` — bridges the observable into the atom-based graph |
|
|
49
|
+
| `useIdentity()` hook | ~20 | plugin-assistant (Chat), plugin-thread, plugin-comments, plugin-markdown (collab identity), plugin-code, plugin-script, plugin-space (SpacePresence), plugin-transcription, plugin-onboarding (WelcomeScreen), plugin-client (Account/Profile containers) |
|
|
50
|
+
| `halo.createIdentity(profile?)` | 14 | Operation handler (`plugin-client/operations/create-identity`), testing (`initializeIdentity`), ~10 story fixtures, import scripts (plugin-inbox, plugin-onboarding) |
|
|
51
|
+
| `halo.recoverIdentity({...})` | 4 | plugin-client `account login` and `halo recover` CLI commands |
|
|
52
|
+
| `halo.updateProfile(profile)` | 1 | plugin-client `ProfileContainer` |
|
|
53
|
+
| `identity.did` / `identityKey` / `spaceKey` / `profile` | many | Field reads wherever identity is obtained |
|
|
54
|
+
|
|
55
|
+
### 2.2 Devices
|
|
56
|
+
|
|
57
|
+
| API | Count | Consumers |
|
|
58
|
+
| ------------------------------------------ | ----- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
59
|
+
| `halo.device` | 6 | plugin-client `device info/update` commands, plugin-calls (presence key), plugin-iroh-beacon (`deviceKey`) |
|
|
60
|
+
| `halo.devices.get()` / `useDevices()` | 4 | plugin-client `DevicesContainer`, `device list` command, plugin-onboarding (`onboarding-manager` — device count gates recovery hints) |
|
|
61
|
+
| `Device`, `DeviceKind`, `DeviceType` types | 3 | plugin-client device UI/CLI formatting |
|
|
62
|
+
| `DevicesService.updateDevice` (raw RPC) | 1 | plugin-client `device update` command — no proxy-level API exists |
|
|
63
|
+
|
|
64
|
+
### 2.3 Credentials & recovery
|
|
65
|
+
|
|
66
|
+
| API | Count | Consumers |
|
|
67
|
+
| ---------------------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `halo.credentials.subscribe()` / `useCredentials()` | 3 | plugin-client `credential list` command, `RecoveryCredentialsContainer` (filters `IdentityRecovery` credentials) |
|
|
69
|
+
| `halo.queryCredentials({ type })` | 1 | plugin-client `credential list` |
|
|
70
|
+
| `halo.writeCredentials([...])` | 2 | plugin-client `credential add`; plugin-script (writes a `ServiceAccess` credential before deploy) |
|
|
71
|
+
| `space.internal.getCredentials()` | 1 | plugin-client `credential list --space` |
|
|
72
|
+
| `SpacesService.queryCredentials` (raw RPC) | 1 | composer-app `util/halo.ts` `queryAllCredentials` — works around `queryCredentials` being sync/incomplete |
|
|
73
|
+
| `IdentityService.createRecoveryCredential` (raw RPC) | 3 | plugin-client `create-recovery-code`, `create-passkey` operations |
|
|
74
|
+
| `IdentityService.requestRecoveryChallenge` (raw RPC) | 1 | plugin-client `redeem-passkey` operation |
|
|
75
|
+
| `IdentityService.recoverIdentity` (raw RPC) | 2 | plugin-client `redeem-passkey`, `redeem-token` operations (proof/token variants not on the proxy) |
|
|
76
|
+
|
|
77
|
+
The recovery/passkey flows reach past `client.halo` into raw RPC services because the proxy API
|
|
78
|
+
is incomplete — a clear signal for what the new service must cover as first-class verbs.
|
|
79
|
+
|
|
80
|
+
### 2.4 EDGE identity (credential presentation)
|
|
81
|
+
|
|
82
|
+
`createEdgeIdentity(client)` builds an `EdgeIdentity` whose `presentCredentials({ challenge })`
|
|
83
|
+
signs a nonce challenge with the device key — HALO's auth handshake toward EDGE.
|
|
84
|
+
|
|
85
|
+
| Consumer | Site |
|
|
86
|
+
| ---------------- | --------------------------------------------------------------------- |
|
|
87
|
+
| plugin-assistant | `edge-model-resolver` (AI service auth) |
|
|
88
|
+
| plugin-connector | `connector-coordinator` (EDGE websocket auth) |
|
|
89
|
+
| plugin-payments | `services/edge-auth` (`Authorization: VerifiablePresentation` header) |
|
|
90
|
+
| plugin-client | `useHubClient`, `edge status` command |
|
|
91
|
+
|
|
92
|
+
### 2.5 Space management
|
|
93
|
+
|
|
94
|
+
Space CRUD/lifecycle — HALO-adjacent because membership and space keys are credential-backed:
|
|
95
|
+
|
|
96
|
+
| API | Count | Consumers |
|
|
97
|
+
| ---------------------------------------------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
98
|
+
| `client.spaces.get()` / `.get(id)` | 57 | Ubiquitous space resolution: plugin-space, plugin-assistant, plugin-inbox, plugin-wnfs, plugin-search, plugin-thread, plugin-meeting, plugin-connector, plugin-discord/bluesky/slack sync, plugin-deck, plugin-simple-layout, composer-app recovery |
|
|
99
|
+
| `client.spaces.create(props?, options?)` | 34 | plugin-space `operations/create`, personal-space bootstrap (`identity-created`, `initializeIdentity`, `halo create` — with `tags`, `membershipPolicy`), plugin-native-filesystem mirror space, ~20 story fixtures |
|
|
100
|
+
| `client.spaces.subscribe()` | 8 | plugin-space (`spaces-ready` ×3, `save-tracker`, `JoinDialog`), plugin-client (`capabilities/client.ts`), plugin-routine (`trigger-runtime-controller`) |
|
|
101
|
+
| `client.spaces.join(code)` | 1 | plugin-space `space join` command |
|
|
102
|
+
| `client.spaces.import(archive, opts?)` | 4 | plugin-space `operations/import-space`, plugin-onboarding exemplar space, plugin-assistant snapshot testing |
|
|
103
|
+
| `space.waitUntilReady()` | ~20 | Everywhere a space is created/joined/opened |
|
|
104
|
+
| `space.state.get()` / `SpaceState` | 4 | plugin-doctor, plugin-space `react-surface` |
|
|
105
|
+
| `useSpaces()` / `useSpace(id)` | ~80 | plugin-space containers + story fixtures across ~25 plugins |
|
|
106
|
+
| `space.internal.export({ format })` | 3 | plugin-space `operations/export-space`, onboarding/inbox scripts |
|
|
107
|
+
| `space.internal.migrate()` / `db.runMigrations` | 3 | plugin-space `operations/migrate`, `spaces-ready`; plugin-client `migrations` |
|
|
108
|
+
| `space.internal.setEdgeReplicationPreference` / `data.edgeReplication` | 6 | plugin-space (`operations/create`, `spaces-ready`, sync-status UI), plugin-client `halo create` |
|
|
109
|
+
| `space.internal.db.saveStateChanged` | 2 | plugin-space `save-tracker` |
|
|
110
|
+
| `AppSpace.*` (`@dxos/app-toolkit`) | ~48 | `getPersonalSpace`, `getActiveSpace`, `isPersonalSpace`, tags — an existing app-level façade over `client.spaces` |
|
|
111
|
+
|
|
112
|
+
Note: `space.db` / ECHO query APIs (the 268 `@dxos/react-client/echo` imports) are the ECHO
|
|
113
|
+
migration track (`Database` service in `@dxos/echo`), not HALO — they are out of scope here
|
|
114
|
+
except where noted.
|
|
115
|
+
|
|
116
|
+
### 2.6 Membership & presence
|
|
117
|
+
|
|
118
|
+
| API | Count | Consumers |
|
|
119
|
+
| --------------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
120
|
+
| `useMembers(spaceKey)` | 7 | plugin-space (`SpacePresence`, `SpaceHomeDashboard`), plugin-thread (Channel/Thread), plugin-comments, plugin-transcription |
|
|
121
|
+
| `SpaceMember` type / `PresenceState` | 6 | plugin-space `members` command + presence UI, plugin-thread |
|
|
122
|
+
| `member.presence`, `member.identity` | 6 | Same |
|
|
123
|
+
| `SpaceMemberList` (`@dxos/shell/react`) | 2 | plugin-space `MembersContainer` (annotated `TODO: Copied from Shell`) |
|
|
124
|
+
|
|
125
|
+
No consumer calls role management (`updateMemberRole`) or member removal today — the UI simply
|
|
126
|
+
doesn't offer it. The new API should still define these verbs (Keyhive makes revocation
|
|
127
|
+
first-class).
|
|
128
|
+
|
|
129
|
+
### 2.7 Invitations
|
|
130
|
+
|
|
131
|
+
Device (HALO) invitations:
|
|
132
|
+
|
|
133
|
+
| API | Consumers |
|
|
134
|
+
| ------------------------------ | ----------------------------------------------------------------------- |
|
|
135
|
+
| `client.halo.share(opts?)` | plugin-client `DevicesContainer` (QR + auth code), `halo share` command |
|
|
136
|
+
| `client.halo.join(invitation)` | plugin-client `halo join`, `account login` commands |
|
|
137
|
+
|
|
138
|
+
Space invitations:
|
|
139
|
+
|
|
140
|
+
| API | Consumers |
|
|
141
|
+
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
142
|
+
| `space.share({ type, authMethod, multiUse, target })` | plugin-space `operations/share`, `operations/get-share-link` (delegated + known-public-key), `MembersContainer` (interactive + delegated), `space share` command |
|
|
143
|
+
| `client.spaces.join(code)` | plugin-space `space join` command |
|
|
144
|
+
| `useSpaceInvitations(spaceKey)` | plugin-space `MembersContainer` |
|
|
145
|
+
|
|
146
|
+
Shared machinery:
|
|
147
|
+
|
|
148
|
+
| API | Consumers |
|
|
149
|
+
| ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
150
|
+
| `InvitationEncoder.encode/decode` | 12 sites (plugin-client, plugin-space) |
|
|
151
|
+
| `Invitation.State` (state-machine polling) | 20 sites — CLI `waitForState`, dialog state UIs |
|
|
152
|
+
| `Invitation.Type` (`INTERACTIVE`/`DELEGATED`), `Invitation.AuthMethod` (`SHARED_SECRET`/`KNOWN_PUBLIC_KEY`) | plugin-space share/get-share-link, operation schemas (`Schema.Enums(Invitation.Type)`) |
|
|
153
|
+
| `CancellableInvitationObservable` / `AuthenticatingInvitationObservable` | Devices/Members containers, CLI utils |
|
|
154
|
+
| `hostInvitation` helper | plugin-client `halo share`, plugin-space `space share` |
|
|
155
|
+
| `InvitationResult` | Join dialogs (plugin-client, plugin-space) |
|
|
156
|
+
|
|
157
|
+
### 2.8 Existing verb layer (key observation)
|
|
158
|
+
|
|
159
|
+
Composer already routes most HALO _mutations_ through `Operation` definitions rather than
|
|
160
|
+
calling the client ad hoc:
|
|
161
|
+
|
|
162
|
+
- `plugin-client` operations: `CreateIdentity`, `JoinIdentity`, `ShareIdentity`,
|
|
163
|
+
`RecoverIdentity`, `CreateRecoveryCode`, `CreatePasskey`, `RedeemPasskey`, `RedeemToken`,
|
|
164
|
+
`ResetStorage`, `CreateAgent`.
|
|
165
|
+
- `plugin-space` operations: `Create`, `Join`, `Share`, `GetShareLink`, `ImportSpace`,
|
|
166
|
+
`ExportSpace`, `Migrate`, `Rename`, `OpenMembers`, …
|
|
167
|
+
|
|
168
|
+
The operation _handlers_ are where `client.*` calls concentrate. What is **not** funneled:
|
|
169
|
+
reads and subscriptions (`identity.get()`, `useIdentity`, `useSpaces`, `useMembers`,
|
|
170
|
+
`spaces.subscribe`), invitation observables, types/enums, and the raw-RPC escape hatches
|
|
171
|
+
(§2.3). Replacing the client dependency therefore mostly means giving the handlers, hooks, and
|
|
172
|
+
types a new home — not redesigning plugin control flow.
|
|
173
|
+
|
|
174
|
+
### 2.9 Out of scope
|
|
175
|
+
|
|
176
|
+
- `plugin-devtools` — deliberately introspects the whole client stack; migrates last (or stays
|
|
177
|
+
on `@dxos/client` as a diagnostic tool).
|
|
178
|
+
- `SystemService.reset`, `client.diagnostics()` — client lifecycle, not HALO.
|
|
179
|
+
- ECHO database APIs (`space.db`, `useQuery`, …) — separate `@dxos/echo` Database service track.
|
|
180
|
+
- `plugin-observability` identity provider — reads raw services for telemetry; follows once the
|
|
181
|
+
services exist.
|
|
182
|
+
|
|
183
|
+
## 3. Evaluation: replacing the surface with `@dxos/halo` services
|
|
184
|
+
|
|
185
|
+
### 3.1 Shape
|
|
186
|
+
|
|
187
|
+
**`@dxos/halo` holds definitions only — no implementations.** The package exports service tags
|
|
188
|
+
(`Context.Tag`), their interfaces, verb functions (module-level `Effect`s that consume the tag),
|
|
189
|
+
and the Effect schemas for the domain types. Every concrete backing — the client adapter, the
|
|
190
|
+
Keyhive/EDGE layers, any in-memory test double — lives in a **separate implementation package**
|
|
191
|
+
and is provided as a `Layer` at composition time. This keeps `@dxos/halo` free of `@dxos/client`
|
|
192
|
+
(and of `keyhive_wasm`) and makes the backend a layer swap. (The Keyhive membership prototype has
|
|
193
|
+
been removed from this package to honor this rule; a future `@dxos/halo-keyhive` will host that
|
|
194
|
+
runtime — see §3.5.)
|
|
195
|
+
|
|
196
|
+
Two services (`Context.Tag`s) plus a serviceless `Invitation` module:
|
|
197
|
+
|
|
198
|
+
- **`Invitation`** — invitation _types + flow verbs_ only (no service tag): the `Flow` handle,
|
|
199
|
+
its event stream, authenticate/cancel/code. It does **not** initiate invitations nor query the
|
|
200
|
+
active set.
|
|
201
|
+
- **`Identity`** — identity & device management, **plus** device invitations: _initiation_
|
|
202
|
+
(`share`/`join`) and _querying_ (`invitations`, a current-value stream). Uses `Invitation`'s
|
|
203
|
+
`Flow`.
|
|
204
|
+
- **`Space`** — space management & membership, **plus** space invitations: _initiation_
|
|
205
|
+
(`share`/`join`) and _querying_ (`invitations(id)`, a current-value stream). Uses
|
|
206
|
+
`Invitation`'s `Flow`.
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
Identity ─────┐
|
|
210
|
+
├──▶ Invitation (types + flow verbs; depends on neither)
|
|
211
|
+
Space ────────┘
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { Effect } from 'effect';
|
|
216
|
+
import { Identity, Invitation, Space } from '@dxos/halo';
|
|
217
|
+
|
|
218
|
+
// Initiation lives on Identity/Space; the returned flow is an Invitation.Flow:
|
|
219
|
+
const program = Effect.gen(function* () {
|
|
220
|
+
const flow = yield* Space.share(space.id); // Space.Service → Invitation.Flow
|
|
221
|
+
yield* Invitation.authenticate(flow, code); // flow verbs drive the lifecycle (no service)
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The split matches the audit: identity/device consumers (§2.1–2.4) rarely touch spaces;
|
|
226
|
+
space-resolution consumers (§2.5–2.6) rarely touch identity; the invitation _lifecycle_ (§2.7) is
|
|
227
|
+
a self-contained state-machine world. Putting _initiation_ on `Identity`/`Space` means a consumer
|
|
228
|
+
that only shares a space depends on `Space` (+ `Invitation`) without pulling in identity, while
|
|
229
|
+
`Invitation` stays a leaf dependency reusable by both.
|
|
230
|
+
|
|
231
|
+
Reactivity: the current API mixes `MulticastObservable`, callbacks, and React hooks. The
|
|
232
|
+
services expose each reactive read as an `Effect` (snapshot) plus a `Stream`/`Atom` (changes)
|
|
233
|
+
— plugin-client already bridges to atoms via `CreateAtom.fromObservable`, and the app graph is
|
|
234
|
+
atom-based, so `Atom` is the natural currency for the React layer.
|
|
235
|
+
|
|
236
|
+
### 3.2 `Identity.Service` — identity & device management
|
|
237
|
+
|
|
238
|
+
| Verb | Replaces |
|
|
239
|
+
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
240
|
+
| `Identity.identity` → `Stream<Option<Profile>>` (current-value) | `halo.identity.get()`, `halo.identity.subscribe()`, `useIdentity` |
|
|
241
|
+
| `Identity.create(profile?)` | `halo.createIdentity()` |
|
|
242
|
+
| `Identity.updateProfile(profile)` | `halo.updateProfile()` |
|
|
243
|
+
| `Identity.recover({ code \| token \| proof })` | `halo.recoverIdentity()` + raw `IdentityService.recoverIdentity` (§2.3) |
|
|
244
|
+
| `Identity.createRecoveryKey({ key, algorithm, lookupKey })` | raw `IdentityService.createRecoveryCredential` (recovery code + passkey) |
|
|
245
|
+
| `Identity.requestRecoveryChallenge()` | raw `IdentityService.requestRecoveryChallenge` |
|
|
246
|
+
| `Identity.devices` → `Stream` (current-value) | `halo.devices`, `useDevices` |
|
|
247
|
+
| `Identity.localDevice` | `halo.device` |
|
|
248
|
+
| `Identity.updateDevice(profile)` | raw `DevicesService.updateDevice` |
|
|
249
|
+
| `Identity.attest({ challenge, audience? })` | `createEdgeIdentity(client).presentCredentials()` (§2.4) — signed, audience-bound message per MIGRATION.md §5.1 |
|
|
250
|
+
| `Identity.share(opts?)` → `Invitation.Flow` | `client.halo.share()` — device-invitation initiation (delegates the lifecycle to `Invitation`) |
|
|
251
|
+
| `Identity.join(code)` → `Invitation.Flow` | `client.halo.join()` — device/account join |
|
|
252
|
+
|
|
253
|
+
Types: device kind and the identity profile fields are inlined into `Identity` (`DeviceKind`,
|
|
254
|
+
`DeviceInfo`, `Info`) rather than living in separate schema modules. The legacy `Identity` proxy
|
|
255
|
+
type (`did`, `identityKey`, `spaceKey`, `profile`) collapses into `Info` = `{ did, displayName? }`
|
|
256
|
+
— `spaceKey` consumers (composer-app `queryAllCredentials`, plugin-onboarding `util.ts`) are
|
|
257
|
+
credential plumbing, deferred with the credential verbs below.
|
|
258
|
+
|
|
259
|
+
The table above is the full target surface. The **first shipped cut** (this PR) implements the
|
|
260
|
+
common path — `identity`, `create`, `recover`, `updateProfile`, `devices`, `share`/`join`,
|
|
261
|
+
`invitations` (each reactive read a single current-value stream). The remaining verbs are
|
|
262
|
+
**deferred** to keep the package definitions-only and
|
|
263
|
+
lean: `createRecoveryKey` / `requestRecoveryChallenge`, `localDevice` / `updateDevice`, `attest`,
|
|
264
|
+
and the transitional `credentials` / `writeCredential` pair. Credentials in particular dissolve
|
|
265
|
+
into Keyhive membership ops and would drag the protobuf `Credential` type into this
|
|
266
|
+
definitions-only package, so recovery/credential flows stay on the raw `IdentityService` until
|
|
267
|
+
that type is modeled here. The plugin-script `ServiceAccess` credential should become an explicit
|
|
268
|
+
capability-grant verb when the EDGE shim lands.
|
|
269
|
+
|
|
270
|
+
### 3.3 `Space.Service` — space management & membership
|
|
271
|
+
|
|
272
|
+
| Verb | Replaces |
|
|
273
|
+
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
274
|
+
| `Space.spaces` → `Stream` (current-value) | `client.spaces.get()`, `client.spaces.subscribe()`, `useSpaces` |
|
|
275
|
+
| `Space.get(id)` → `Option<Space>` | `client.spaces.get(id)`, `useSpace(id)` |
|
|
276
|
+
| `Space.create(props?, { tags?, membershipPolicy? })` | `client.spaces.create()` |
|
|
277
|
+
| `Space.waitReady(id)` (or `Space.state(id): Stream`) | `space.waitUntilReady()`, `space.state.get()`, `SpaceState` |
|
|
278
|
+
| `Space.update(id, { name, icon, hue, tags })` | direct property writes (plugin-space rename) |
|
|
279
|
+
| `Space.members(id)` → `Stream` (current-value) | `useMembers`, `SpaceMember` reads |
|
|
280
|
+
| `Space.updateMemberRole(id, subject, role)` / `Space.removeMember(id, subject)` | Keyhive delegation/revocation (no current consumer). Shipped: adapter maps to `space.updateMemberRole` (member resolved by DID) |
|
|
281
|
+
| `Space.export(id, { format })` / `Space.import(archive, opts?)` | `space.internal.export()`, `client.spaces.import()` |
|
|
282
|
+
| `Space.migrate(id)` | `space.internal.migrate()`, `db.runMigrations` |
|
|
283
|
+
| `Space.setReplication(id, setting)` / `Space.replication(id)` | `space.internal.setEdgeReplicationPreference`, `data.edgeReplication` (becomes "delegate `pull` to EDGE" under Keyhive, MIGRATION.md §5.1) |
|
|
284
|
+
| `Space.share(id, { type, authMethod, multiUse, target })` → `Invitation.Flow` | `space.share()` — space-invitation initiation (delegates the lifecycle to `Invitation`) |
|
|
285
|
+
| `Space.join(code)` → `Invitation.Flow` | `client.spaces.join()` |
|
|
286
|
+
|
|
287
|
+
The returned `Space` value is a plain data snapshot (id, state, properties, tags) — **not** a
|
|
288
|
+
live proxy owning `db`. Database access stays on the `@dxos/echo` `Database` service keyed by
|
|
289
|
+
`SpaceId`, which decouples the two migrations. `AppSpace` (`@dxos/app-toolkit`) keeps its role
|
|
290
|
+
as the app-level façade (personal space, active space) but re-implements over `Space.Service`,
|
|
291
|
+
which converts its ~48 call sites for free.
|
|
292
|
+
|
|
293
|
+
Roles map forward to Keyhive `Access` (`read`/`edit`/`admin`/`pull`) per MIGRATION.md §3;
|
|
294
|
+
member `presence` is not credential data and should move to a presence/network concern rather
|
|
295
|
+
than ride on `SpaceMember`.
|
|
296
|
+
|
|
297
|
+
### 3.4 `Invitation` — invitation flow types & verbs (no service)
|
|
298
|
+
|
|
299
|
+
The `Invitation` module is **types + flow-level verbs only** — there is no service tag.
|
|
300
|
+
Initiation is `Identity.share`/`join` (§3.2) and `Space.share`/`join` (§3.3), which construct and
|
|
301
|
+
return an `Invitation.Flow`; querying the active set is `Identity.invitations` (device) and
|
|
302
|
+
`Space.invitations` (space). The `Flow` handle carries its own lifecycle, so its verbs need no
|
|
303
|
+
service. `Invitation` depends on neither `Identity` nor `Space`.
|
|
304
|
+
|
|
305
|
+
| Verb / type | Replaces |
|
|
306
|
+
| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
307
|
+
| `Invitation.Flow`: `{ events: Stream<InvitationEvent>; id }` | `CancellableInvitationObservable`, `AuthenticatingInvitationObservable` (the object `Identity`/`Space` initiation returns) |
|
|
308
|
+
| `Invitation.authenticate(flow, code)` | `observable.authenticate()` / `Invitation.State` polling |
|
|
309
|
+
| `Invitation.cancel(flow)` | `observable.cancel()` |
|
|
310
|
+
| `Invitation.events(flow)` → `Stream<InvitationEvent>` | `Invitation.State` polling, `hostInvitation`/`waitForState` helpers |
|
|
311
|
+
| `Space.invitations(id)` / `Identity.invitations` (+ `*Changes`) | `useSpaceInvitations` / `useHaloInvitations` |
|
|
312
|
+
| `Invitation.code(flow)` / decode at `Identity.join`/`Space.join` | `InvitationEncoder` |
|
|
313
|
+
| Schemas: `InvitationKind`, `AuthMethod`, `Type`, `InvitationEvent` (tagged union) | `Invitation.State/Type/AuthMethod` protobuf enums (also used in operation schemas via `Schema.Enums`) |
|
|
314
|
+
|
|
315
|
+
Modeling the flow as a `Stream` of tagged events (`connecting` → `readyForAuth(authCode?)` →
|
|
316
|
+
`success(result)` | `cancelled` | `error`) removes every `state >= Invitation.State.X`
|
|
317
|
+
comparison in the dialogs and the CLI's hand-rolled `waitForState`. Under Keyhive the lifecycle
|
|
318
|
+
is unchanged — only what `Identity`/`Space` initiation does behind the flow differs: `share`
|
|
319
|
+
mints an ephemeral invitation key or prekey delegation; `join`/`accept` becomes contact-card
|
|
320
|
+
exchange + delegation (MIGRATION.md §4.6). Keeping the `Flow` state machine in one leaf module
|
|
321
|
+
lets both initiation paths and every dialog/CLI consumer share exactly one shape.
|
|
322
|
+
|
|
323
|
+
### 3.5 Layering & migration mechanics
|
|
324
|
+
|
|
325
|
+
`@dxos/halo` is **definitions only** (tags, verbs, schemas); implementations are separate
|
|
326
|
+
packages provided as `Layer`s at one composition root — today `plugin-client`, which already
|
|
327
|
+
owns client construction. Plugins import only `@dxos/halo`:
|
|
328
|
+
|
|
329
|
+
```text
|
|
330
|
+
@dxos/halo Identity.Service / Space.Service (+ Invitation types/flow verbs) (tags + verbs + schemas, NO impl)
|
|
331
|
+
▲ ▲
|
|
332
|
+
│ │ provide layers at composition:
|
|
333
|
+
│ │
|
|
334
|
+
@dxos/halo-adapter-client ── layerClient(client) (wraps HaloProxy/EchoProxy/InvitationsProxy)
|
|
335
|
+
@dxos/halo-keyhive ── layerKeyhive / layerEdge (future) (Keyhive prototype impl relocates here, MIGRATION.md §A.2)
|
|
336
|
+
▲
|
|
337
|
+
│
|
|
338
|
+
plugin-client ────── composition root; sole remaining importer of @dxos/client
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`layerClient` (in `@dxos/halo-adapter-client`) wraps the existing proxies (`HaloProxy`,
|
|
342
|
+
`EchoProxy` spaces, `InvitationsProxy`) — a mechanical adapter, no behavior change. Later,
|
|
343
|
+
`layerKeyhive` (or the EDGE-endpoint shim) replaces it without touching a single plugin. Because
|
|
344
|
+
`@dxos/halo` carries no implementation, this is a pure layer swap; the Keyhive membership runtime
|
|
345
|
+
will live in a future `@dxos/halo-keyhive`, not in `@dxos/halo`.
|
|
346
|
+
|
|
347
|
+
Suggested sequence:
|
|
348
|
+
|
|
349
|
+
1. **Define services** in `@dxos/halo` (tags, verbs, event/error schemas — no layers). Inline the
|
|
350
|
+
value types each service needs (`DeviceKind`/`Info`, `Access`/`Member`, `InvitationEvent`,
|
|
351
|
+
`Invitation.Flow`) rather than shipping separate schema modules. Keep the Keyhive membership
|
|
352
|
+
runtime out of `@dxos/halo` (destined for `@dxos/halo-keyhive`).
|
|
353
|
+
2. **`layerClient`** in a `@dxos/halo-adapter-client` package (keeps `@dxos/halo` free of the
|
|
354
|
+
client dependency), provided by `plugin-client` alongside — then instead of —
|
|
355
|
+
`ClientCapabilities.Client`. **(Implemented — see `@dxos/halo-adapter-client`, with an
|
|
356
|
+
integration suite in `@dxos/halo-e2e`.)**
|
|
357
|
+
3. **Port operation handlers** (§2.8) — the concentrated mutation sites — from `client.*` to
|
|
358
|
+
service verbs. Plugin control flow (operations) is unchanged.
|
|
359
|
+
4. **Port hooks**: reimplement `useIdentity`/`useDevices`/`useSpaces`/`useSpace`/`useMembers`/
|
|
360
|
+
`useSpaceInvitations`/`useCredentials` over the services' atoms (in `@dxos/halo-react` or
|
|
361
|
+
`app-toolkit`), then mass-update imports. `AppSpace` converts in the same step.
|
|
362
|
+
5. **Port dialogs/CLI** off invitation observables onto flow streams; delete `waitForState`
|
|
363
|
+
and the copied Shell components' state plumbing.
|
|
364
|
+
6. **Stories & testing**: replace `client.halo.createIdentity()` + `client.spaces.create()`
|
|
365
|
+
fixtures (~30 sites) with `Identity.create`/`Space.create` over an in-memory test layer —
|
|
366
|
+
stories stop needing a full `Client` at all, currently the single largest source of
|
|
367
|
+
incidental `@dxos/client` imports.
|
|
368
|
+
7. **Sweep types**: `Identity`/`Device`/`SpaceMember`/`Credential`/`Invitation` type imports →
|
|
369
|
+
`@dxos/halo` schemas. At this point `@dxos/client` remains only inside `plugin-client`'s
|
|
370
|
+
composition root and `plugin-devtools`.
|
|
371
|
+
|
|
372
|
+
### 3.6 Gaps & open questions
|
|
373
|
+
|
|
374
|
+
1. **`Credential` surface** — `RecoveryCredentialsContainer` and the `credential` CLI render
|
|
375
|
+
raw credentials. Transitional verbs (§3.2) cover it, but the rendering will need a schema
|
|
376
|
+
change when credentials become Keyhive membership ops. Acceptable: the container is
|
|
377
|
+
diagnostic UI.
|
|
378
|
+
2. **`space.internal.db.*`** (`saveStateChanged`, `runMigrations`) — database concerns reached
|
|
379
|
+
through the space proxy; they belong to the ECHO `Database` service track and should be
|
|
380
|
+
excluded from `Space.Service` rather than ported.
|
|
381
|
+
3. **Presence** — `SpaceMember.presence` conflates membership (HALO) with liveness (MESH).
|
|
382
|
+
Proposal: `Space.members` returns membership only; presence moves to a separate
|
|
383
|
+
stream/service so the Keyhive backend isn't forced to fake it.
|
|
384
|
+
4. **`PublicKey` vs DID** — consumers compare `identityKey`/`spaceKey` (`PublicKey`). New
|
|
385
|
+
schemas should standardize on DID / `SpaceId` strings (per this package's types); the
|
|
386
|
+
adapter translates during the transition.
|
|
387
|
+
5. **Enums in operation schemas** — `Schema.Enums(Invitation.Type)` leaks protobuf enums into
|
|
388
|
+
operation contracts (`plugin-space/operations/definitions.ts`); these need parallel Effect
|
|
389
|
+
schema literals and a coordinated cut-over.
|
|
390
|
+
6. **Shell UI components** — `SpaceMemberList`/`InvitationList`/`AuthCode` from
|
|
391
|
+
`@dxos/shell/react` embed observable-based logic; they should be rebuilt on the new hooks in
|
|
392
|
+
`react-ui-*` (the `MembersContainer` TODO already wants this).
|
|
393
|
+
7. **Definitions-only invariant** — `@dxos/halo` must not gain a runtime dependency. The Keyhive
|
|
394
|
+
membership prototype (`make`/`layerMemory`) has been removed for this reason and will live in
|
|
395
|
+
`@dxos/halo-keyhive` (§3.5); a lint/dep-cruiser rule should keep `@dxos/halo` from importing
|
|
396
|
+
anything but schema and type primitives so no future layer sneaks back in.
|