@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.
@@ -0,0 +1,343 @@
1
+ # Consumer Migration — `@dxos/client` HALO → `@dxos/halo` services
2
+
3
+ How to move Composer (`packages/apps/composer-app`) and the plugins
4
+ (`packages/plugins/*`) off direct `@dxos/client` HALO access and onto the
5
+ `@dxos/halo` Effect services (backed by `@dxos/halo-adapter-client`).
6
+
7
+ Companion to [API_AUDIT.md](./API_AUDIT.md) (what the consumers use today) and
8
+ [MIGRATION.md](./MIGRATION.md) (the HALO → Keyhive backend rewrite). This
9
+ document is the practical cookbook: the decisive verdict on coverage, then
10
+ before→after patterns for the cases the services already handle.
11
+
12
+ ## Verdict
13
+
14
+ **The new API cannot yet replace `@dxos/client` everywhere in Composer and the
15
+ plugins.** The two services (`Identity`, `Space`) — plus the serviceless
16
+ `Invitation` flow types/verbs — cover the imperative HALO surface:
17
+ identity/device lifecycle, space management and membership, and the invitation
18
+ state machine (initiation and querying live on `Identity`/`Space`). The
19
+ `layerClient(client)`
20
+ composition root drops in wherever a plugin already holds a `Client`. That is
21
+ enough to migrate imperative call sites (operation handlers, CLI commands,
22
+ managers, import scripts).
23
+
24
+ It is **not** enough to migrate the codebase wholesale. Four gaps remain, each
25
+ with an agreed disposition:
26
+
27
+ 1. **React bindings — `@dxos/halo-react`.** There are ~368 uses of
28
+ `useIdentity` / `useDevices` / `useCredentials` / `useSpaces` / `useSpace` /
29
+ `useMembers` / `useSpaceInvitations` across 163 files — the bulk of the
30
+ consumer surface. The new [`@dxos/halo-react`](../halo-react) hook library
31
+ provides drop-in equivalents backed by the HALO services (each reactive read
32
+ is a current-value `Stream` surfaced through `@effect-atom` atoms and
33
+ `useAtomValue`). Components migrate hook-by-hook
34
+ by swapping the import and wrapping the tree in `HaloProvider` instead of
35
+ `ClientProvider`; see [Migrating React hooks](#migrating-react-hooks). Hooks
36
+ with no service verb yet (`useCredentials`) stay on `@dxos/react-client`.
37
+ 2. **ECHO database access — a separate track, by design.** `Space.Info` is a
38
+ plain snapshot, not a live proxy — it has no `.db`, `.crud`, `.properties`,
39
+ or `.internal`. The heavy `space.db.*` / `space.internal.*` usage is ECHO,
40
+ not HALO, and belongs on the `@dxos/echo` `Database` service keyed by
41
+ `SpaceId`. This is not folded into HALO. See API_AUDIT.md §3.6.
42
+ 3. **Deferred verbs — planned, not yet defined.** Credentials
43
+ (`halo.queryCredentials` / `writeCredentials`), recovery-credential
44
+ _creation_ (`createRecoveryCredential` / `requestRecoveryChallenge`), EDGE
45
+ `createEdgeIdentity` / attestation, and device _update_ will be added as
46
+ `Identity`-service verbs in a follow-up. Consumers of those stay on the
47
+ client until then.
48
+ 4. **Space create/lifecycle gaps — mostly closed.** `Space.create` now accepts
49
+ `{ name?, tags?, membershipPolicy? }`, and EDGE replication is toggled with
50
+ `Space.setEdgeReplication(id, setting)`. `migrate` is intentionally left out.
51
+ `State` remains the consumer-relevant subset (`inactive` / `closed` /
52
+ `ready`), not the full `SpaceState` enum.
53
+
54
+ So: migrate imperative HALO call sites now; leave React components and ECHO
55
+ access on `@dxos/client` until the binding rewrite and the ECHO `Database`
56
+ service land, and the deferred verbs (§3) until they are defined. The rest of
57
+ this doc covers the cases that _can_ move.
58
+
59
+ ## Composition root
60
+
61
+ Every plugin already obtains its `Client` from the
62
+ `ClientCapabilities.Client` capability provided by `plugin-client` — a single
63
+ injection point. Build the HALO layer once from that client and provide it to
64
+ any effect:
65
+
66
+ ```ts
67
+ import { layerClient } from '@dxos/halo-adapter-client';
68
+ import { Identity, Space } from '@dxos/halo';
69
+
70
+ const halo = layerClient(client); // Layer<Identity.Service | Space.Service>
71
+
72
+ const program = Effect.gen(function* () {
73
+ // Reactive reads are current-value streams; take the head for a one-shot snapshot.
74
+ const identity = yield* Stream.runHead(Identity.identity);
75
+ const spaces = yield* Stream.runHead(Space.spaces);
76
+ // ...
77
+ });
78
+
79
+ await Effect.runPromise(program.pipe(Effect.provide(halo)));
80
+ ```
81
+
82
+ `layerClient` is the seam that replaces direct `@dxos/client` access for HALO
83
+ concerns. The backend behind it (legacy client, EDGE shim, or Keyhive) can swap
84
+ without touching consumers.
85
+
86
+ ## Migrating React hooks
87
+
88
+ Most consumer usage is React hooks, not imperative calls. The
89
+ [`@dxos/halo-react`](../halo-react) package mirrors `@dxos/react-client`'s
90
+ HALO/space hooks, backed by the services. Migration is mechanical: swap the
91
+ provider, then swap imports hook-by-hook.
92
+
93
+ **1. Swap the provider.** `@dxos/halo-react` reads the services off a React
94
+ context holding an Effect `Context.Context<Identity | Space | Invitation>`.
95
+ Build that context once (from the client adapter, or any backing) and wrap the
96
+ tree:
97
+
98
+ ```tsx
99
+ import { Context } from 'effect';
100
+ import { Identity, Space } from '@dxos/halo';
101
+ import { makeIdentityService, makeSpaceService } from '@dxos/halo-adapter-client';
102
+ import { HaloProvider } from '@dxos/halo-react';
103
+
104
+ const services = Context.empty().pipe(
105
+ Context.add(Identity.Service, makeIdentityService(client)),
106
+ Context.add(Space.Service, makeSpaceService(client)),
107
+ );
108
+
109
+ // Replaces <ClientProvider> for HALO concerns.
110
+ <HaloProvider services={services}>{children}</HaloProvider>;
111
+ ```
112
+
113
+ In a plugin this context is built once at the `plugin-client` composition root
114
+ (where the `Client` capability lives) and provided app-wide.
115
+
116
+ **2. Swap the hooks.** Same names, same call shape; the returned values are the
117
+ HALO snapshot types (`Identity.Info`, `Space.Info`, `Space.Member`,
118
+ `Invitation.Flow`) instead of the client proxy types.
119
+
120
+ | `@dxos/react-client` hook | `@dxos/halo-react` hook | Return type | Notes |
121
+ | --------------------------------- | --------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------- |
122
+ | `useIdentity(): Identity \| null` | `useIdentity(): Identity.Info \| undefined` | `{ did, displayName? }` | `null` → `undefined`; `did` replaces `identityKey`/`spaceKey` |
123
+ | `useDevices(): Device[]` | `useDevices(): readonly Identity.DeviceInfo[]` | `{ key, kind?, label?, current }` | — |
124
+ | `useSpaces({ all? }): Space[]` | `useSpaces({ all? }): readonly Space.Info[]` | `{ id, name?, state }` | same `all` option and `ready`-only default |
125
+ | `useSpace(id \| key): Space` | `useSpace(id?: SpaceId): Space.Info \| undefined` | snapshot | `SpaceId` only — no `PublicKey` |
126
+ | `useMembers(key): SpaceMember[]` | `useMembers(id?: SpaceId): readonly Space.Member[]` | `{ did?, role, online }` | takes a `SpaceId`, not a key; `role` is `Space.Access` |
127
+ | `useSpaceInvitations(id \| key)` | `useSpaceInvitations(id?: SpaceId): readonly Invitation.Flow[]` | `Invitation.Flow[]` | drive each flow via the `Invitation` verbs |
128
+ | `useHaloInvitations()` | `useHaloInvitations(): readonly Invitation.Flow[]` | `Invitation.Flow[]` | — |
129
+ | `useCredentials()` | _not yet_ | — | no credential verb yet (§3) — stays on `@dxos/react-client` |
130
+
131
+ ```tsx
132
+ // Before
133
+ import { useIdentity } from '@dxos/react-client/halo';
134
+ import { useSpaces, useMembers } from '@dxos/react-client/echo';
135
+
136
+ // After
137
+ import { useIdentity, useSpaces, useMembers } from '@dxos/halo-react';
138
+ ```
139
+
140
+ **3. ECHO stays put.** `useQuery` and other `@dxos/react-client/echo` database
141
+ hooks are ECHO, not HALO — they do not move here (§2).
142
+
143
+ For imperative call sites (operation handlers, CLI commands), use the service
144
+ verbs directly rather than hooks — those patterns follow.
145
+
146
+ ## Patterns
147
+
148
+ Each pattern shows the current `@dxos/client` call and its `@dxos/halo`
149
+ equivalent. All effects require the corresponding service in `R`; provide
150
+ `layerClient(client)`.
151
+
152
+ ### Identity — read / gate
153
+
154
+ ```ts
155
+ // Before
156
+ const identity = client.halo.identity.get();
157
+ if (!identity) return;
158
+ const did = identity.did;
159
+
160
+ // After — reads are current-value streams; take the head for a one-shot read
161
+ const maybe = Option.flatten(yield * Stream.runHead(Identity.identity)); // Option<Identity.Info>
162
+ if (Option.isNone(maybe)) return;
163
+ const did = maybe.value.did;
164
+ ```
165
+
166
+ Field reads change shape: `Identity.Info` is `{ did, displayName? }`.
167
+ `identityKey` / `spaceKey` (credential plumbing) are intentionally dropped in
168
+ favor of the DID.
169
+
170
+ ### Identity — subscribe
171
+
172
+ ```ts
173
+ // Before
174
+ const sub = client.halo.identity.subscribe((identity) => onChange(identity));
175
+ // ... sub.unsubscribe();
176
+
177
+ // After — the same stream, subscribed instead of head-read; completes when the scope closes
178
+ yield * Stream.runForEach(Identity.identity, (maybe) => Effect.sync(() => onChange(maybe)));
179
+ ```
180
+
181
+ ### Identity — create / update profile
182
+
183
+ ```ts
184
+ // Before
185
+ await client.halo.createIdentity({ displayName: 'Alice' });
186
+ await client.halo.updateProfile({ displayName: 'Alice B.' });
187
+
188
+ // After
189
+ const info = yield * Identity.create({ displayName: 'Alice', deviceLabel: 'laptop' });
190
+ yield * Identity.updateProfile({ displayName: 'Alice B.' });
191
+ ```
192
+
193
+ ### Devices
194
+
195
+ ```ts
196
+ // Before
197
+ const devices = client.halo.devices.get();
198
+
199
+ // After — Identity.devices is a current-value stream
200
+ const devices = yield * Stream.runHead(Identity.devices); // Option<readonly DeviceInfo[]> ({ key, kind?, label?, current })
201
+ ```
202
+
203
+ `DeviceInfo.kind` is the inlined `Identity.DeviceKind` literal
204
+ (`'unknown' | 'browser' | 'native' | 'mobile' | 'agent' | 'agent-managed'`),
205
+ replacing the protobuf `DeviceType` enum. **Device _update_ is not defined** —
206
+ stay on the client for that.
207
+
208
+ ### Spaces — list / get
209
+
210
+ ```ts
211
+ // Before
212
+ const spaces = client.spaces.get();
213
+ const space = getSpace(object) ?? client.spaces.get(spaceId);
214
+
215
+ // After — Space.spaces is a current-value stream; get is a point lookup
216
+ const spaces = yield * Stream.runHead(Space.spaces); // Option<readonly Space.Info[]>
217
+ const maybe = yield * Space.get(spaceId); // Option<Space.Info>
218
+ ```
219
+
220
+ `Space.Info` is `{ id, name?, state }` — a snapshot. For `space.db` / query /
221
+ properties, use `@dxos/echo` keyed by `Space.Info.id`; do not expect a proxy.
222
+
223
+ ### Spaces — create
224
+
225
+ ```ts
226
+ // Before
227
+ const space = await client.spaces.create({ name: 'Project' }, { tags: ['team'], membershipPolicy });
228
+ await space.internal.setEdgeReplicationPreference(EdgeReplicationSetting.ENABLED);
229
+
230
+ // After
231
+ const info = yield * Space.create({ name: 'Project', tags: ['team'], membershipPolicy: 'invite' });
232
+ yield * Space.setEdgeReplication(info.id, 'enabled');
233
+ ```
234
+
235
+ `membershipPolicy` is `'invite' | 'locked'` and `setEdgeReplication` takes
236
+ `'disabled' | 'enabled'` (the adapter maps the legacy protobuf enums). **Gap:**
237
+ `migrate` is intentionally not modeled — call sites needing it stay on the
238
+ client.
239
+
240
+ ### Spaces — wait ready
241
+
242
+ ```ts
243
+ // Before
244
+ await space.waitUntilReady();
245
+
246
+ // After
247
+ yield * Space.waitReady(spaceId);
248
+ ```
249
+
250
+ ### Members
251
+
252
+ ```ts
253
+ // Before
254
+ const members = space.members.get(); // SpaceMember[]
255
+
256
+ // After — Space.members(id) is a current-value stream
257
+ const members = yield * Stream.runHead(Space.members(spaceId)); // Option<readonly Space.Member[]> ({ did?, role, online })
258
+ ```
259
+
260
+ `Member.role` is the Keyhive-aligned `Space.Access`
261
+ (`'pull' | 'read' | 'edit' | 'admin'`); the adapter maps the legacy
262
+ `SpaceMember.Role` enum. `updateMemberRole(id, did, role)` and
263
+ `removeMember(id, did)` are defined (removal re-keys the space and does not
264
+ converge in the in-memory test harness — see the e2e NOTE).
265
+
266
+ ### Export / import
267
+
268
+ ```ts
269
+ // Before — via client space handle
270
+ // After
271
+ const archive = yield* Space.exportSpace(spaceId); // { filename, contents }
272
+ const info = yield* Space.importSpace(archive, { tags: [...] });
273
+ ```
274
+
275
+ ### Invitations — share (host)
276
+
277
+ Initiation lives on `Identity` / `Space` (not on `Invitation`); the returned
278
+ `Flow` is then driven through the `Invitation` verbs.
279
+
280
+ ```ts
281
+ // Before
282
+ const observable = space.share({ authMethod: Invitation.AuthMethod.NONE });
283
+ observable.subscribe((invitation) => {
284
+ /* switch on invitation.state */
285
+ });
286
+ const code = InvitationEncoder.encode(observable.get());
287
+
288
+ // After
289
+ const flow = yield * Space.share(spaceId, { authMethod: 'none' }); // Invitation.Flow
290
+ const code = yield * Invitation.code(flow);
291
+ yield *
292
+ Stream.runForEach(Invitation.events(flow), (event) =>
293
+ Effect.sync(() => {
294
+ // event._tag: 'connecting' | 'connected' | 'readyForAuthentication'
295
+ // | 'authenticating' | 'success' | 'cancelled' | 'error'
296
+ }),
297
+ );
298
+ ```
299
+
300
+ Device invitations are identical with `Identity.share(options?)`.
301
+
302
+ ### Invitations — join (guest) + authenticate
303
+
304
+ ```ts
305
+ // Before
306
+ const observable = client.spaces.join(InvitationEncoder.decode(code));
307
+ observable.subscribe(/* ... */);
308
+ await observable.authenticate(authCode);
309
+
310
+ // After
311
+ const flow = yield * Space.join(code); // decodes internally
312
+ yield *
313
+ Stream.runForEach(Invitation.events(flow), (event) =>
314
+ event._tag === 'readyForAuthentication' ? Invitation.authenticate(flow, authCode) : Effect.void,
315
+ );
316
+ ```
317
+
318
+ `Identity.join(code)` is the device equivalent. `Invitation.cancel(flow)` aborts.
319
+
320
+ ### Invitations — observe active flows
321
+
322
+ ```ts
323
+ // Before
324
+ const invitations = space.invitations.get();
325
+
326
+ // After — querying lives on the owning service as a current-value stream
327
+ const flows = yield * Stream.runHead(Space.invitations(spaceId)); // device: Identity.invitations
328
+ ```
329
+
330
+ Drive an individual `Invitation.Flow` through the flow verbs (`Invitation.events`
331
+ / `authenticate` / `cancel` / `code`), which need no service.
332
+
333
+ ## What stays on `@dxos/client` (for now)
334
+
335
+ | Area | Reason | Unblocks when |
336
+ | --------------------------------------------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------ |
337
+ | `useCredentials` | no service verb yet | credential verbs added to `Identity` (the other HALO hooks now live in `@dxos/halo-react`) |
338
+ | `space.db` / `space.crud` / `space.properties` / `space.internal.*` (except edge replication) | ECHO, not HALO | migrate to the `@dxos/echo` `Database` service keyed by `SpaceId` |
339
+ | `halo.queryCredentials` / `writeCredentials`; recovery-credential _creation_ | deferred verb | credential + recovery verbs added to `Identity` |
340
+ | `createEdgeIdentity` / EDGE attestation | deferred verb | EDGE verbs added to `Identity` |
341
+ | device _update_ | deferred verb | `Identity` device-update verb added |
342
+ | `space.internal.migrate` | intentionally out of scope | (revisit if needed) |
343
+ | full `SpaceState` enum | `Space.State` is the consumer subset | (by design) |
package/LICENSE ADDED
@@ -0,0 +1,105 @@
1
+ # Functional Source License, Version 1.1, ALv2 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-Apache-2.0
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 DXOS
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the Apache License, Version 2.0 that is effective on the second anniversary of
91
+ the date we make the Software available. On or after that date, you may use the
92
+ Software under the Apache License, Version 2.0, in which case the following
93
+ will apply:
94
+
95
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
96
+ this file except in compliance with the License.
97
+
98
+ You may obtain a copy of the License at
99
+
100
+ http://www.apache.org/licenses/LICENSE-2.0
101
+
102
+ Unless required by applicable law or agreed to in writing, software distributed
103
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
+ specific language governing permissions and limitations under the License.