@abloatai/humans 0.59.2 → 0.61.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.
Files changed (96) hide show
  1. package/README.md +1 -1
  2. package/dist/Ablo.d.ts +2 -10
  3. package/dist/Ablo.js +0 -1
  4. package/dist/client.d.ts +1 -48
  5. package/dist/humans.d.ts +1 -1
  6. package/dist/local/BaseSyncedStore.d.ts +5 -5
  7. package/dist/local/BaseSyncedStore.js +7 -7
  8. package/dist/local/Database.d.ts +2 -2
  9. package/dist/local/LazyReferenceCollection.d.ts +1 -1
  10. package/dist/local/Model.js +46 -56
  11. package/dist/local/NetworkMonitor.js +2 -0
  12. package/dist/local/RuntimeContext.js +2 -0
  13. package/dist/local/SyncClient.d.ts +12 -36
  14. package/dist/local/SyncClient.js +50 -108
  15. package/dist/local/client/createModelOperations.d.ts +3 -27
  16. package/dist/local/client/createModelOperations.js +20 -21
  17. package/dist/local/client/options.d.ts +14 -39
  18. package/dist/local/client/reactiveEngine.d.ts +3 -9
  19. package/dist/local/client/reactiveEngine.js +6 -151
  20. package/dist/local/client/storeLifecycle.js +5 -1
  21. package/dist/local/fileUploads.d.ts +27 -0
  22. package/dist/local/fileUploads.js +55 -0
  23. package/dist/local/storeContract.d.ts +5 -5
  24. package/dist/local/stores/syncAction.d.ts +4 -4
  25. package/dist/local/sync/contextOnChange.js +1 -1
  26. package/dist/local/sync/createClaimStream.js +1 -1
  27. package/dist/local/sync/credentialLifecycle.d.ts +4 -5
  28. package/dist/local/sync/credentialLifecycle.js +4 -5
  29. package/dist/local/sync/deltaPipeline.js +12 -6
  30. package/dist/local/sync/schemas.d.ts +10 -10
  31. package/dist/local/sync/scopeGroups.d.ts +11 -0
  32. package/dist/local/sync/scopeGroups.js +75 -0
  33. package/dist/local/sync/wsFrameHandlers.d.ts +1 -1
  34. package/dist/local/transactions/localMutation.js +3 -3
  35. package/dist/local/transactions/mutations/MutationQueue.d.ts +4 -5
  36. package/dist/local/transactions/mutations/MutationQueue.js +25 -51
  37. package/dist/local/transactions/mutations/batchProcessing.js +23 -10
  38. package/dist/local/transactions/mutations/commitPayload.d.ts +9 -2
  39. package/dist/local/transactions/mutations/commitTransport.js +3 -1
  40. package/dist/local/transactions/mutations/executionSelection.d.ts +0 -1
  41. package/dist/local/transactions/mutations/executionSelection.js +9 -17
  42. package/dist/local/transactions/mutations/failureHandling.js +9 -0
  43. package/dist/local/transactions/mutations/localMutation.js +3 -3
  44. package/dist/local/transactions/mutations/queueCoalescing.js +8 -0
  45. package/dist/local/transactions/mutations/replayValidation.d.ts +15 -15
  46. package/dist/react/AbloProvider.d.ts +11 -86
  47. package/dist/react/AbloProvider.js +11 -163
  48. package/dist/react/ClientSideSuspense.d.ts +1 -1
  49. package/dist/react/DefaultFallback.d.ts +1 -1
  50. package/dist/react/createAbloReact.js +1 -1
  51. package/dist/react/useErrorListener.js +1 -1
  52. package/dist/react/useMutationFailureListener.js +1 -1
  53. package/dist/react.d.ts +1 -1
  54. package/dist/react.js +1 -1
  55. package/dist/surface.d.ts +3 -3
  56. package/dist/surface.js +1 -4
  57. package/package.json +3 -3
  58. package/src/Ablo.ts +5 -17
  59. package/src/client.ts +0 -51
  60. package/src/local/BaseSyncedStore.ts +14 -14
  61. package/src/local/Model.ts +45 -55
  62. package/src/local/NetworkMonitor.ts +2 -0
  63. package/src/local/RuntimeContext.ts +2 -0
  64. package/src/local/SyncClient.ts +73 -140
  65. package/src/local/client/createModelOperations.ts +30 -64
  66. package/src/local/client/options.ts +20 -43
  67. package/src/local/client/reactiveEngine.ts +7 -179
  68. package/src/local/client/storeLifecycle.ts +6 -1
  69. package/src/local/fileUploads.ts +97 -0
  70. package/src/local/storeContract.ts +5 -5
  71. package/src/local/sync/contextOnChange.ts +1 -1
  72. package/src/local/sync/createClaimStream.ts +1 -1
  73. package/src/local/sync/credentialLifecycle.ts +4 -5
  74. package/src/local/sync/deltaPipeline.ts +10 -6
  75. package/src/local/sync/scopeGroups.ts +91 -0
  76. package/src/local/sync/wsFrameHandlers.ts +0 -1
  77. package/src/local/transactions/localMutation.ts +3 -3
  78. package/src/local/transactions/mutations/MutationQueue.ts +24 -53
  79. package/src/local/transactions/mutations/batchProcessing.ts +25 -10
  80. package/src/local/transactions/mutations/commitPayload.ts +11 -1
  81. package/src/local/transactions/mutations/commitTransport.ts +2 -2
  82. package/src/local/transactions/mutations/executionSelection.ts +9 -15
  83. package/src/local/transactions/mutations/failureHandling.ts +10 -0
  84. package/src/local/transactions/mutations/localMutation.ts +3 -3
  85. package/src/local/transactions/mutations/queueCoalescing.ts +6 -0
  86. package/src/react/AbloProvider.tsx +17 -249
  87. package/src/react/useErrorListener.ts +1 -1
  88. package/src/react/useMutationFailureListener.ts +1 -1
  89. package/src/react.ts +1 -5
  90. package/src/surface.ts +1 -4
  91. package/dist/local/sync/participants.d.ts +0 -132
  92. package/dist/local/sync/participants.js +0 -342
  93. package/dist/local/transactions/mutations/pendingDrain.d.ts +0 -33
  94. package/dist/local/transactions/mutations/pendingDrain.js +0 -117
  95. package/src/local/sync/participants.ts +0 -564
  96. package/src/local/transactions/mutations/pendingDrain.ts +0 -169
@@ -6,7 +6,7 @@
6
6
  * `read` and `list`, with the same point lookup restricted to the local graph under
7
7
  * `local`, the writes `create`, `update`, and `delete`, the coordination
8
8
  * namespace `claim` (callable as `claim({ id })`, plus `claim.state`,
9
- * `claim.queue`, `claim.release`, and `claim.reorder`), `join`, and `onChange`.
9
+ * `claim.queue`, `claim.release`, and `claim.reorder`), and `onChange`.
10
10
  * The factory returns a plain object; the client assembles the `ablo.<model>`
11
11
  * lookup table from one of these per model.
12
12
  */
@@ -66,7 +66,6 @@ import type { ModelRegistry } from '../ModelRegistry.js';
66
66
  import type { InstanceCache } from '../InstanceCache.js';
67
67
  import type { SyncClient } from '../SyncClient.js';
68
68
  import type { OnDemandLoader } from '../sync/OnDemandLoader.js';
69
- import type { JoinedParticipant } from '../sync/participants.js';
70
69
  import { ModelScope } from '@abloatai/transaction/types';
71
70
  import type {
72
71
  Duration,
@@ -112,7 +111,6 @@ export type {
112
111
  ModelCreateParams,
113
112
  ModelUpdateParams,
114
113
  ModelDeleteParams,
115
- JoinOptions,
116
114
  } from '@abloatai/transaction/client/resources/modelOperations';
117
115
  export type { Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, HeldLease };
118
116
 
@@ -126,7 +124,6 @@ import type {
126
124
  ClaimAttemptEvent,
127
125
  ClaimQueueView,
128
126
  ClaimReorderParams,
129
- JoinOptions,
130
127
  LocalCountOptions,
131
128
  LocalReadOptions,
132
129
  ModelCreateManyParams,
@@ -152,6 +149,9 @@ import {
152
149
  type ReadSetContext,
153
150
  } from '@abloatai/transaction/internal/read-set';
154
151
 
152
+ const ignoreSeparatelyObservedMutationFailure = (): undefined => undefined;
153
+ const ignoreBestEffortClaimReleaseFailure = (): undefined => undefined;
154
+
155
155
  export interface ModelClientMeta {
156
156
  readonly key: string;
157
157
  readonly typename: string;
@@ -284,17 +284,6 @@ export interface ModelCollaboration {
284
284
  * fire-and-forget, best-effort semantics as `enterScope`.
285
285
  */
286
286
  pinScope?(scope: Record<string, string>): void | Promise<void>;
287
- /**
288
- * Opens a presence and claim subscription on this model's sync group(s) and
289
- * returns the live participant handle. Backs `ablo.<model>.join(ids)`.
290
- * WebSocket only, since presence needs a live socket; it is absent on other
291
- * client constructions, where the surface throws a clear error.
292
- */
293
- createJoin?(
294
- modelKey: string,
295
- ids: string | readonly string[],
296
- options?: JoinOptions,
297
- ): Promise<JoinedParticipant>;
298
287
  }
299
288
 
300
289
 
@@ -375,26 +364,6 @@ interface ReactiveModelSurface<T, Fields = T> {
375
364
  */
376
365
  claim: ClaimApi<T, Fields>;
377
366
 
378
- /**
379
- * Joins the sync group(s) for one or more rows of this model and returns a
380
- * live participant handle — presence (`.peers`), the scoped claim stream
381
- * (`.claims`), and `.leave()` / `await using` disposal. This is a presence
382
- * subscription: it reports who else is here and what they hold, not row
383
- * values changing — for the latter, use `onChange`.
384
- *
385
- * WebSocket only: presence needs a live socket, so this is absent on HTTP
386
- * clients and throws on any non-WebSocket construction.
387
- *
388
- * ```ts
389
- * await using participant = await ablo.sections.join(sectionIds, { ttl: '5m' });
390
- * participant.peers; // who else is here
391
- * ```
392
- */
393
- join(
394
- ids: string | readonly string[],
395
- options?: JoinOptions,
396
- ): Promise<JoinedParticipant>;
397
-
398
367
  /** Subscribe to changes; the callback runs on every change. */
399
368
  onChange(
400
369
  callback: (entities: T[]) => void,
@@ -524,7 +493,7 @@ export function createModelOperations<T, C>(
524
493
  // await does not create an unhandled-rejection process error. Returning
525
494
  // the original promise preserves normal rejection for callers that do
526
495
  // await or attach their own catch handler.
527
- void confirmation.catch(() => undefined);
496
+ void confirmation.catch(ignoreSeparatelyObservedMutationFailure);
528
497
  return confirmation;
529
498
  };
530
499
  };
@@ -546,7 +515,10 @@ export function createModelOperations<T, C>(
546
515
  return rows.map((row) => modelAsRow<T>(row));
547
516
  };
548
517
 
549
- const waitForMutation = async (model: Model): Promise<void> => {
518
+ const waitForMutation = async (
519
+ model: Model,
520
+ exactConfirmation?: Promise<void>,
521
+ ): Promise<void> => {
550
522
  // Model writes are optimistic locally, but their promise has one stable
551
523
  // meaning: authoritative confirmation. Callers that do not need the
552
524
  // barrier can keep using the row immediately and leave the promise to the
@@ -558,7 +530,8 @@ export function createModelOperations<T, C>(
558
530
  // coalescer and producing one SQL transaction per delta.
559
531
  await Promise.resolve();
560
532
  await syncClient.syncNow();
561
- await syncClient.waitForConfirmation(model.getModelName(), model.id);
533
+ if (exactConfirmation) await exactConfirmation;
534
+ else await syncClient.waitForConfirmation(model.getModelName(), model.id);
562
535
  };
563
536
 
564
537
  // Claims this model surface currently holds, keyed by the exact grant id.
@@ -690,9 +663,9 @@ export function createModelOperations<T, C>(
690
663
  // This runs after authoritative confirmation. A best-effort abandon frame
691
664
  // cannot turn a committed write into an apparent failure; the server has
692
665
  // already fulfilled the participant's claims as part of that commit.
693
- await releaseClaimsForEntity(entityId).catch(() => undefined);
666
+ await releaseClaimsForEntity(entityId).catch(ignoreBestEffortClaimReleaseFailure);
694
667
  if (explicit && !explicitWasLocal) {
695
- await explicit.release?.().catch(() => undefined);
668
+ await explicit.release?.().catch(ignoreBestEffortClaimReleaseFailure);
696
669
  }
697
670
  };
698
671
 
@@ -1402,11 +1375,11 @@ export function createModelOperations<T, C>(
1402
1375
  }
1403
1376
  : {}),
1404
1377
  };
1405
- syncClient.add(model, effective);
1406
- await waitForMutation(model);
1378
+ const confirmation = syncClient.add(model, effective);
1379
+ await waitForMutation(model, confirmation);
1407
1380
  return modelAsRow<T>(model);
1408
1381
  } finally {
1409
- await autoLease?.release?.().catch(() => {});
1382
+ await autoLease?.release?.().catch(ignoreBestEffortClaimReleaseFailure);
1410
1383
  }
1411
1384
  });
1412
1385
 
@@ -1528,8 +1501,12 @@ export function createModelOperations<T, C>(
1528
1501
  : {}),
1529
1502
  };
1530
1503
  model.applyChanges(patch);
1531
- syncClient.update(model, effective);
1532
- await waitForMutation(model);
1504
+ const confirmation = syncClient.update(
1505
+ model,
1506
+ effective,
1507
+ patch,
1508
+ );
1509
+ await waitForMutation(model, confirmation);
1533
1510
  return modelAsRow<T>(model);
1534
1511
  },
1535
1512
  });
@@ -1585,8 +1562,12 @@ export function createModelOperations<T, C>(
1585
1562
  // the server. (`updateFromData` is the hydration path and would discard
1586
1563
  // the tracking, producing an empty `input: {}` no-op mutation.)
1587
1564
  model.applyChanges(params.data);
1588
- syncClient.update(model, effective);
1589
- await waitForMutation(model);
1565
+ const confirmation = syncClient.update(
1566
+ model,
1567
+ effective,
1568
+ params.data,
1569
+ );
1570
+ await waitForMutation(model, confirmation);
1590
1571
  const updated = modelAsRow<T>(model);
1591
1572
  await settleClaimsAfterWrite(id, handle);
1592
1573
  return updated;
@@ -1658,8 +1639,8 @@ export function createModelOperations<T, C>(
1658
1639
  ...opts,
1659
1640
  ...(selected ? { claimRef: { id: selected.id } } : {}),
1660
1641
  };
1661
- syncClient.delete(model, effective);
1662
- await waitForMutation(model);
1642
+ const confirmation = syncClient.delete(model, effective);
1643
+ await waitForMutation(model, confirmation);
1663
1644
  await settleClaimsAfterWrite(id, handle);
1664
1645
  }),
1665
1646
 
@@ -1667,21 +1648,6 @@ export function createModelOperations<T, C>(
1667
1648
  // readers (`claim.state` / `claim.queue` / `claim.release` / `claim.reorder`).
1668
1649
  claim: claimApi,
1669
1650
 
1670
- join: guard(
1671
- (
1672
- ids: string | readonly string[],
1673
- options?: JoinOptions,
1674
- ): Promise<JoinedParticipant> => {
1675
- if (!collaboration?.createJoin) {
1676
- throw new AbloValidationError(
1677
- `Model "${schemaKey}" was built without a WebSocket runtime, so join() is unavailable here. Presence needs a live socket — use the standard Ablo({ schema, apiKey }) client (not the HTTP transport).`,
1678
- { code: 'model_join_not_configured' },
1679
- );
1680
- }
1681
- return collaboration.createJoin(schemaKey, ids, options);
1682
- },
1683
- ),
1684
-
1685
1651
  onChange(callback, options): () => void {
1686
1652
  return autorun(() => {
1687
1653
  callback(local.list(options));
@@ -32,6 +32,11 @@ import type { CommitOutboxScope } from '@abloatai/transaction/commit';
32
32
  */
33
33
  export type { CredentialProvider } from '@abloatai/transaction/auth/apiKey';
34
34
  import type { CredentialProvider } from '@abloatai/transaction/auth/apiKey';
35
+ import type {
36
+ SessionCredential,
37
+ SessionEndpoint,
38
+ SessionProvider,
39
+ } from '@abloatai/transaction/sessions';
35
40
  import type { AbloPlugin } from '../../plugin.js';
36
41
  import type { ParticipantKind } from '@abloatai/transaction/types/participant';
37
42
 
@@ -74,19 +79,21 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
74
79
  * `ABLO_API_KEY` environment variable, so you usually pass nothing. A
75
80
  * long-lived key needs no refresh; the client uses it as-is.
76
81
  *
77
- * - **An async resolver** `() => Promise<string | null>` the escape hatch for
78
- * when the exchange needs custom headers, a request body, or a non-HTTP mint
79
- * (vault rotation, a cloud token service, an existing auth session). It uses
80
- * the same renewal machinery as the endpoint form.
81
- *
82
- * The endpoint and resolver forms share one contract: return a token; return
83
- * `null` when the login itself is gone (terminal — the client signs out and
84
- * fails `ready()` with `session_expired`); or throw on a transient failure, which
85
- * backs off and retries without signing out. The endpoint form maps HTTP onto
86
- * this for you: only a structured `401 session_expired` means signed out.
82
+ * - **An async resolver** for advanced process-owned key rotation, such as a
83
+ * vault or workload-identity exchange. Scoped actor renewal belongs in
84
+ * `session` instead.
87
85
  */
88
86
  apiKey?: string | CredentialProvider | null | undefined;
89
87
 
88
+ /**
89
+ * Scoped actor identity. Pass a session returned by `sessions.create()` for
90
+ * bounded work, a provider that re-mints it for a long-lived client, or
91
+ * `{ endpoint: '/api/ablo-session' }` in a browser. Endpoint responses use
92
+ * the canonical credential protocol; only a structured `401
93
+ * session_expired` ends the underlying login.
94
+ */
95
+ session?: SessionCredential | SessionProvider | SessionEndpoint | null | undefined;
96
+
90
97
  /**
91
98
  * Pins this client to one Ablo project. During `ready()` the server resolves
92
99
  * the API key's actual project and the client refuses to start when it differs.
@@ -105,33 +112,6 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
105
112
  */
106
113
  branchId?: string | null | undefined;
107
114
 
108
- /**
109
- * The session-mint endpoint — the browser-side auth field, and the named
110
- * endpoint for the route that mints the signed-in user's short-lived token:
111
- *
112
- * ```ts
113
- * const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
114
- * ```
115
- *
116
- * The client owns the whole exchange: it POSTs the route (same-origin, cookies
117
- * included), validates the canonical auth response contract, keeps it fresh
118
- * ahead of expiry, and re-mints when the server reports the token stale. Only
119
- * a structured `401 session_expired` response means signed out. It also
120
- * accepts an async resolver `() => Promise<string | null>` when the exchange
121
- * needs custom headers or a body — the same contract as the resolver form of
122
- * `apiKey`.
123
- *
124
- * Mutually exclusive with `apiKey`: a server holds a key, a browser holds a mint
125
- * route, and passing both is a validation error.
126
- */
127
- authEndpoint?: string | CredentialProvider | null | undefined;
128
-
129
- /** Timeout for a session-mint request. @default 10000 */
130
- authTimeoutMs?: number | undefined;
131
-
132
- /** Explicit opt-in for a cross-origin session-mint endpoint. */
133
- allowCrossOriginAuthEndpoint?: boolean | undefined;
134
-
135
115
  /**
136
116
  * Local persistence mode. Pass `indexeddb` only when you want offline
137
117
  * queueing and a reload-surviving browser cache.
@@ -264,15 +244,12 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
264
244
  */
265
245
  apiKey?: string | CredentialProvider | null | undefined;
266
246
 
247
+ /** A scoped session, or a provider that re-mints it for a long-lived client. */
248
+ session?: SessionCredential | SessionProvider | SessionEndpoint | null | undefined;
249
+
267
250
  /** Expected project assertion; see {@link AbloOptions.projectId}. */
268
251
  projectId?: string | null | undefined;
269
252
 
270
- /**
271
- * Session-mint endpoint (string or async resolver) — see
272
- * {@link AbloOptions.authEndpoint}. Mutually exclusive with `apiKey`.
273
- */
274
- authEndpoint?: string | CredentialProvider | null | undefined;
275
-
276
253
  /**
277
254
  * A bearer auth token, sent as `Authorization: Bearer <token>` on every request.
278
255
  *
@@ -2,9 +2,9 @@
2
2
  * The reactive engine assembly (ADR 0016). `Ablo({ ... })` resolves auth and
3
3
  * capabilities; `humans().init` constructs the store cluster; the lifecycle
4
4
  * — first mint, identity, ready() — lives in `./storeLifecycle.ts`. What
5
- * remains here is assembly around those parts: the claim stream and
6
- * participant manager, options validation, the typed model proxies, and the
7
- * commit/claim/session resources — composed into the reactive client.
5
+ * remains here is assembly around those parts: the claim and presence streams,
6
+ * options validation, the typed model proxies, and the
7
+ * commit and claim resources — composed into the reactive client.
8
8
  *
9
9
  * Extracted from the factory so the composition root stays a root: resolve,
10
10
  * dispatch, return. The remaining assembly converts to decoration of a
@@ -12,13 +12,13 @@
12
12
  * design step (docs/plans/package-split.md).
13
13
  */
14
14
 
15
- import type { Schema, SchemaRecord } from '@abloatai/transaction/schema/schema';
15
+ import type { SchemaRecord } from '@abloatai/transaction/schema/schema';
16
16
  import { omittedModelError } from '@abloatai/transaction/schema/select';
17
17
  import {
18
18
  durableCommitOperationSchema,
19
19
  type DurableCommitOperation,
20
20
  } from '@abloatai/transaction/commit';
21
- import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, claimedError } from '@abloatai/transaction/errors';
21
+ import { AbloConnectionError, AbloValidationError, claimedError } from '@abloatai/transaction/errors';
22
22
  import type { ModelTarget, ModelClaim } from '@abloatai/transaction/coordination/schema';
23
23
  import type { BatchFence } from '@abloatai/transaction/coordination';
24
24
  import {
@@ -30,13 +30,6 @@ import {
30
30
  subTarget,
31
31
  } from '@abloatai/transaction/coordination';
32
32
  import { validateAbloOptions } from './validateAbloOptions.js';
33
- import { mintSession } from '@abloatai/transaction/auth/sessionMint';
34
- import type { MintSessionContext } from '@abloatai/transaction/auth/sessionMint';
35
- import {
36
- revokeCapability,
37
- rotateCapability,
38
- } from '@abloatai/transaction/auth/capabilityLifecycle';
39
- import { modelWireNames } from '@abloatai/transaction/auth/capability';
40
33
  import type { StoreCluster } from './storeCluster.js';
41
34
  import { startStoreLifecycle } from './storeLifecycle.js';
42
35
  import type { SyncWebSocket, CoreSyncEventMap } from '../sync/SyncWebSocket.js';
@@ -46,25 +39,19 @@ import {
46
39
  bindClaimLifetime,
47
40
  claimLifetimeOf,
48
41
  } from '@abloatai/transaction/claims/lifetime';
49
- import { createParticipantManager } from '../sync/participants.js';
50
42
  import type { AttachablePresenceStream } from '../../presenceStream.js';
51
43
  import type { ClaimWaitOptions } from '@abloatai/transaction/types/streams';
52
44
  import type { Claim } from '@abloatai/transaction/types/streams';
53
- import type { CredentialProvider } from '@abloatai/transaction/auth/apiKey';
54
45
  import { resolveApiKeyValue, resolveBootstrapBaseUrl } from '@abloatai/transaction/auth/apiKey';
55
46
  import type { AbloOptions } from './options.js';
56
47
  import type { ClientPrelude } from './clientPrelude.js';
57
48
  import type {
58
- AbloSession,
59
49
  ClaimCreateOptions,
60
50
  ClaimResource,
61
51
  CommitCreateOptions,
62
52
  CommitOperationInput,
63
53
  CommitReceipt,
64
54
  CommitResource,
65
- CreateAgentClientParams,
66
- CreateAgentSessionParams,
67
- CreateSessionParams,
68
55
  } from './resourceTypes.js';
69
56
  import {
70
57
  claimAttemptFailure,
@@ -116,12 +103,6 @@ export interface ReactiveEngineInputs<S extends SchemaRecord> extends ClientPrel
116
103
  * assembles around it and constructs none of it.
117
104
  */
118
105
  cluster: StoreCluster;
119
- /**
120
- * Constructs a sibling client (`ablo.agents.create(...)` mints a scoped key
121
- * and builds a second engine with it). Injected by the factory — a direct
122
- * import back into it would close a runtime cycle.
123
- */
124
- createSibling: (options: AbloOptions<S>) => Ablo<S>;
125
106
  }
126
107
 
127
108
  export function buildReactiveEngine<const S extends SchemaRecord>(
@@ -141,7 +122,6 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
141
122
  kind,
142
123
  presence,
143
124
  cluster,
144
- createSibling,
145
125
  } = inputs;
146
126
  const schema = options.schema;
147
127
  const pointReadBaseUrl = resolveBootstrapBaseUrl({
@@ -231,7 +211,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
231
211
  logger.warn(
232
212
  'Ablo: `kind` / `agentId` are ignored when an `apiKey` is configured — ' +
233
213
  'the server derives participant identity from the key’s scope. Remove ' +
234
- 'them (or mint a scoped session via `ablo.sessions.create({ agent })` ' +
214
+ 'them (or mint a scoped session with `Sessions({ schema, apiKey }).create({ agent })` ' +
235
215
  'for a distinct agent identity). They apply only to the self-hosted ' +
236
216
  '`capabilityToken` path.',
237
217
  );
@@ -274,14 +254,6 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
274
254
  });
275
255
  const ready = lifecycle.ready;
276
256
 
277
- const participantManager = createParticipantManager({
278
- ready,
279
- transport,
280
- presence: presenceStream,
281
- claims: claimStream,
282
- schema,
283
- });
284
-
285
257
  // 9b. waitForFlush — drains pending mutations using the store's
286
258
  // pendingChanges counter (already maintained by BaseSyncedStore based
287
259
  // on MutationQueue events). Polls every 50ms; uses the existing
@@ -644,17 +616,6 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
644
616
  // reconcile errors so read interest never makes a read reject or stall.
645
617
  enterScope: (scope) => store.enterScope(scope),
646
618
  pinScope: (scope) => store.pinScope(scope),
647
- // `ablo.<model>.join(ids, { ttl })` performs a scoped participant join
648
- // on this model's sync group(s). WebSocket only — `join` throws
649
- // `AbloConnectionError` if the socket isn't ready.
650
- // `ttl` passes straight through — both surfaces spell the lease the
651
- // same way now, so there is no rename here to make a field's name
652
- // disagree with the value it carries.
653
- createJoin: (modelKey, ids, options) =>
654
- participantManager.join({
655
- scope: { [modelKey]: ids },
656
- ...(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
657
- }),
658
619
  },
659
620
  readSetContext,
660
621
  );
@@ -667,7 +628,7 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
667
628
  await ready();
668
629
  const prepared = prepareReadSet(
669
630
  cluster.readSetContext,
670
- syncClient as object,
631
+ syncClient,
671
632
  commitOptions.readAt,
672
633
  commitOptions.idempotencyKey,
673
634
  commitOptions.reads,
@@ -752,50 +713,6 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
752
713
  },
753
714
  };
754
715
 
755
- /**
756
- * The control-plane credential: always the original configured secret key.
757
- * Never reads `authCredentials` — that holds the exchanged sync credential
758
- * (a wide-scope `rk_` on the hosted path), which control-plane routes
759
- * rightly refuse (e.g. the user-session mint is sk_-gated). Counterpart to
760
- * `getAuthToken()`, which resolves the sync-plane token.
761
- *
762
- * The secret-key-only rule is enforced on the server; the credential-kind taxonomy
763
- * (secret/restricted/ephemeral/publishable) lives in `auth/credentialPolicy`.
764
- */
765
- async function controlPlaneApiKey(): Promise<string | null> {
766
- return resolveApiKeyValue(configuredApiKey);
767
- }
768
-
769
- /**
770
- * Resolve the control-plane context a session/agent mint needs (sk_ +
771
- * bootstrap base URL + the schema-key→typename map the server gates on).
772
- * Shared by `sessions.create` and `agents.create` so the two mint doors
773
- * can never drift on how a token is minted. Throws if no `sk_` is present —
774
- * minting is a backend-only operation.
775
- */
776
- async function buildMintContext(resource: string): Promise<MintSessionContext> {
777
- const apiKey = await controlPlaneApiKey();
778
- if (!apiKey) {
779
- throw new AbloAuthenticationError(
780
- `${resource} requires a secret (sk_) API key — call it from your backend, not the browser.`,
781
- { code: 'apikey_missing' },
782
- );
783
- }
784
- return {
785
- apiKey,
786
- baseUrl: resolveBootstrapBaseUrl({
787
- url,
788
- bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
789
- }),
790
- ...(internalOptions.fetch ? { fetch: internalOptions.fetch } : {}),
791
- // Map every `can` schema-key to the wire typename the server gates on, so a
792
- // typename override (`documents` → `Document`) doesn't mint a capability
793
- // the server then denies. Derived from this client's schema by the one rule
794
- // the HTTP client and the mint route also read. See `MintSessionContext`.
795
- modelTypenames: modelWireNames(schema.models),
796
- };
797
- }
798
-
799
716
  const engine = {
800
717
  ...modelProxies,
801
718
 
@@ -826,11 +743,6 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
826
743
  // is the canonical credential; fall back to a configured API key.
827
744
  //
828
745
  // This is the sync-plane token (bootstrap, WebSocket, query HTTP). Control-plane
829
- // calls (sessions.create, datasource registration) never use it — they
830
- // present the original secret key via `controlPlaneApiKey()` below. The
831
- // split matters: after the startup exchange this resolver returns the
832
- // derived wide-scope `rk_`, a credential the control-plane routes
833
- // correctly refuse (an agent token must never mint humans).
834
746
  return (
835
747
  authCredentials.getAuthToken() ??
836
748
  (await resolveApiKeyValue(configuredApiKey)) ??
@@ -857,90 +769,6 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
857
769
  store.nudgeReconnect();
858
770
  },
859
771
 
860
- sessions: {
861
- // A backend (holding `sk_`) mints a short-lived scoped token for one end
862
- // user or one agent.
863
- //
864
- // Both arms authenticate with the original secret key
865
- // (`controlPlaneApiKey()`), never the wide-scope `rk_` the startup exchange
866
- // installed as the sync credential. A derived agent credential silently
867
- // replacing the secret key on control-plane calls is how humans would get
868
- // minted as agents — and correct attribution is the point.
869
- async create(params: CreateSessionParams<S>): Promise<AbloSession> {
870
- // Both mint paths (`{ user }` → /v1/ephemeral_keys → `ek_`,
871
- // `{ agent, can }` → /v1/capabilities → scoped `rk_`) resolve their
872
- // control-plane context through the shared `buildMintContext`, so this
873
- // client, `agents.create`, and the stateless HTTP client can't drift on
874
- // how a token is minted.
875
- return mintSession(params, await buildMintContext('sessions.create'));
876
- },
877
- async revoke({ id }) {
878
- const context = await buildMintContext('sessions.revoke');
879
- return revokeCapability({
880
- apiKey: context.apiKey,
881
- baseUrl: context.baseUrl,
882
- id,
883
- ...(context.fetch ? { fetch: context.fetch } : {}),
884
- });
885
- },
886
- async rotate({ id, graceSeconds, ttlSeconds }) {
887
- const context = await buildMintContext('sessions.rotate');
888
- return rotateCapability({
889
- apiKey: context.apiKey,
890
- baseUrl: context.baseUrl,
891
- id,
892
- ...(graceSeconds !== undefined ? { graceSeconds } : {}),
893
- ...(ttlSeconds !== undefined ? { ttlSeconds } : {}),
894
- ...(context.fetch ? { fetch: context.fetch } : {}),
895
- });
896
- },
897
- },
898
-
899
- // Mint a scoped agent identity and hand back a connected client bound to it —
900
- // `sessions.create({ agent })` plus a typed `Ablo({ schema, apiKey })` client,
901
- // for agents that run in this (secret-key-holding) process. Omitting `id`
902
- // yields a fresh uuid per call, so concurrent agents are distinct participants
903
- // that queue behind each other (even when they share a `name`). Humans don't
904
- // get a server-built client — ship them a token via `sessions.create({ user })`.
905
- agents: {
906
- async create(params: CreateAgentClientParams<S>): Promise<Ablo<S>> {
907
- // Distinct participant by default: omit `id` → a fresh uuid, so even two
908
- // agents that share a `name` are independent participants and queue
909
- // behind one another. `name` is display only (→ userMeta.name); it never
910
- // derives the id. Pass an explicit `id` only to re-attach an agent to
911
- // its own held claims.
912
- const id = params.id ?? globalThis.crypto.randomUUID();
913
- const userMeta =
914
- params.name !== undefined ? { ...params.userMeta, name: params.name } : params.userMeta;
915
- const sessionParams = {
916
- agent: { id },
917
- can: params.can,
918
- ...(params.onBehalfOf ? { onBehalfOf: params.onBehalfOf } : {}),
919
- ...(params.syncGroups ? { syncGroups: params.syncGroups } : {}),
920
- ...(params.ttlSeconds !== undefined ? { ttlSeconds: params.ttlSeconds } : {}),
921
- ...(userMeta ? { userMeta } : {}),
922
- } satisfies CreateAgentSessionParams<S>;
923
- // Re-mint the `rk_` on every resolver call so a long-lived agent client
924
- // never hits token expiry; the `sk_` stays in this process — the child
925
- // only ever sees its own short-lived `rk_`.
926
- const mintToken = async (): Promise<string> =>
927
- (await mintSession(sessionParams, await buildMintContext('agents.create')))
928
- .token;
929
- // Mint once up front so a bad key / denied scope throws HERE, not later
930
- // inside the child's bootstrap; reuse that first token, re-mint on refresh.
931
- let pending: string | null = await mintToken();
932
- const apiKey: CredentialProvider = async () => {
933
- if (pending !== null) {
934
- const token = pending;
935
- pending = null;
936
- return token;
937
- }
938
- return mintToken();
939
- };
940
- return createSibling({ ...(internalOptions as AbloOptions<S>), apiKey });
941
- },
942
- },
943
-
944
772
  async dispose() {
945
773
  lifecycle.dispose();
946
774
  try {
@@ -143,7 +143,12 @@ export function startStoreLifecycle<S extends SchemaRecord>(
143
143
  // unambiguously a deliberate server client). User-kind clients in Node (an
144
144
  // SSR/RSC module evaluating scaffolded browser code) stay reactive-only.
145
145
  if (credentialResolver) {
146
- const rawEndpoint = internalOptions.authEndpoint ?? internalOptions.apiKey;
146
+ const rawEndpoint =
147
+ internalOptions.session &&
148
+ typeof internalOptions.session === 'object' &&
149
+ 'endpoint' in internalOptions.session
150
+ ? internalOptions.session.endpoint
151
+ : internalOptions.apiKey;
147
152
  const absoluteEndpoint =
148
153
  typeof rawEndpoint === 'string' && /^https?:\/\//i.test(rawEndpoint);
149
154
  store.startCredentialLifecycle(credentialResolver, {