@abloatai/ablo 0.28.0 → 0.29.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/CHANGELOG.md +30 -0
  2. package/dist/Database.js +9 -6
  3. package/dist/SyncClient.d.ts +2 -2
  4. package/dist/SyncClient.js +22 -10
  5. package/dist/adapters/inMemoryStorage.js +18 -14
  6. package/dist/ai-sdk/coordinatedTool.d.ts +15 -2
  7. package/dist/auth/schemas.d.ts +6 -0
  8. package/dist/auth/schemas.js +7 -0
  9. package/dist/cli.cjs +815 -400
  10. package/dist/client/Ablo.d.ts +29 -23
  11. package/dist/client/Ablo.js +8 -196
  12. package/dist/client/claimHeartbeatLoop.d.ts +1 -1
  13. package/dist/client/claimHeartbeatLoop.js +1 -1
  14. package/dist/client/createInternalComponents.d.ts +4 -2
  15. package/dist/client/createInternalComponents.js +3 -1
  16. package/dist/client/durableWrites.d.ts +21 -0
  17. package/dist/client/durableWrites.js +46 -0
  18. package/dist/client/httpClient.d.ts +19 -45
  19. package/dist/client/httpClient.js +104 -26
  20. package/dist/client/httpTransport.d.ts +8 -0
  21. package/dist/client/{ApiClient.js → httpTransport.js} +78 -291
  22. package/dist/client/options.d.ts +43 -19
  23. package/dist/client/options.js +3 -2
  24. package/dist/client/resourceTypes.d.ts +9 -85
  25. package/dist/client/resourceTypes.js +1 -1
  26. package/dist/client/sessionMint.d.ts +5 -6
  27. package/dist/client/sessionMint.js +4 -5
  28. package/dist/client/validateAbloOptions.js +3 -3
  29. package/dist/coordination/schema.d.ts +30 -0
  30. package/dist/coordination/schema.js +14 -0
  31. package/dist/core/index.d.ts +1 -1
  32. package/dist/errors.d.ts +4 -40
  33. package/dist/errors.js +5 -5
  34. package/dist/index.d.ts +14 -8
  35. package/dist/index.js +7 -5
  36. package/dist/interfaces/index.d.ts +3 -0
  37. package/dist/mutators/defineMutators.d.ts +18 -0
  38. package/dist/mutators/defineMutators.js +4 -8
  39. package/dist/react/index.d.ts +1 -1
  40. package/dist/schema/index.d.ts +1 -1
  41. package/dist/schema/index.js +1 -1
  42. package/dist/schema/select.d.ts +15 -0
  43. package/dist/schema/select.js +27 -3
  44. package/dist/source/connector.d.ts +3 -3
  45. package/dist/source/factory.d.ts +4 -6
  46. package/dist/source/factory.js +4 -7
  47. package/dist/source/index.d.ts +4 -4
  48. package/dist/source/index.js +2 -2
  49. package/dist/source/signing.d.ts +0 -3
  50. package/dist/source/types.d.ts +0 -26
  51. package/dist/stores/ObjectStore.js +8 -8
  52. package/dist/surface.d.ts +1 -1
  53. package/dist/surface.js +1 -0
  54. package/dist/sync/BootstrapFetcher.d.ts +10 -0
  55. package/dist/sync/BootstrapFetcher.js +27 -4
  56. package/dist/sync/createClaimStream.d.ts +11 -2
  57. package/dist/sync/createClaimStream.js +4 -3
  58. package/dist/testing/mocks/FakeDatabase.d.ts +1 -1
  59. package/dist/transactions/TransactionQueue.d.ts +2 -2
  60. package/dist/transactions/TransactionQueue.js +3 -3
  61. package/dist/transactions/commitEnvelope.d.ts +1 -1
  62. package/dist/transactions/commitEnvelope.js +2 -2
  63. package/dist/transactions/commitOutboxStore.d.ts +19 -19
  64. package/dist/transactions/durableWriteStore.d.ts +123 -0
  65. package/dist/transactions/durableWriteStore.js +30 -0
  66. package/dist/transactions/httpCommitEnvelope.d.ts +1 -0
  67. package/dist/transactions/httpCommitEnvelope.js +14 -12
  68. package/dist/transactions/idempotencyKey.d.ts +10 -0
  69. package/dist/transactions/idempotencyKey.js +9 -0
  70. package/dist/types/global.d.ts +10 -29
  71. package/dist/types/global.js +4 -7
  72. package/dist/types/streams.d.ts +2 -28
  73. package/dist/wire/frames.d.ts +6 -257
  74. package/dist/wire/frames.js +4 -25
  75. package/dist/wire/index.d.ts +4 -3
  76. package/dist/wire/index.js +2 -2
  77. package/dist/wire/protocolVersion.d.ts +30 -17
  78. package/dist/wire/protocolVersion.js +34 -18
  79. package/docs/agents.md +8 -3
  80. package/docs/api.md +6 -4
  81. package/docs/client-behavior.md +6 -3
  82. package/docs/coordination.md +2 -3
  83. package/docs/data-sources.md +5 -8
  84. package/docs/guarantees.md +21 -0
  85. package/docs/integration-guide.md +1 -0
  86. package/docs/migration.md +21 -1
  87. package/docs/quickstart.md +11 -0
  88. package/docs/react.md +0 -46
  89. package/examples/README.md +6 -5
  90. package/llms.txt +15 -15
  91. package/package.json +2 -7
  92. package/dist/client/ApiClient.d.ts +0 -177
  93. package/dist/commit/contract.d.ts +0 -493
  94. package/dist/commit/contract.js +0 -187
  95. package/dist/commit/index.d.ts +0 -6
  96. package/dist/commit/index.js +0 -5
@@ -1,8 +1,9 @@
1
1
  /**
2
- * The stateless API client behind `Ablo({ apiKey })`. It carries no schema,
3
- * object pool, local database, or WebSocket, and maps the public Model, Claim,
4
- * and Commit nouns directly to HTTP routes on the server. This is the transport
5
- * used for server-side agents, workers, and serverless code.
2
+ * Private HTTP protocol client behind `Ablo({ schema, transport: 'http' })`.
3
+ * It carries no object pool, local database, or WebSocket and maps Model,
4
+ * Claim, and Commit protocol shapes directly to server routes. The typed
5
+ * facade in `httpClient.ts` is the application boundary; this module owns
6
+ * transport envelopes, watermarks, replay, and route details.
6
7
  */
7
8
  import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloIdempotencyError, AbloValidationError, AbloNotFoundError, claimedError, translateHttpError, } from '../errors.js';
8
9
  import { z } from 'zod';
@@ -11,10 +12,11 @@ import { reconcileFunctionalUpdate, } from './functionalUpdate.js';
11
12
  import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfCliKeyMismatch, warnIfDatabaseUrlEnvIgnored, warnIfDatabaseUrlDeprecated, } from './auth.js';
12
13
  import { registerDataSource } from './registerDataSource.js';
13
14
  import { PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER } from '../wire/protocolVersion.js';
14
- import { toMs, toSeconds } from '../utils/duration.js';
15
+ import { toMs } from '../utils/duration.js';
15
16
  import { heartbeatCadenceMs, resolveHeartbeatOptions, startClaimHeartbeatLoop, } from './claimHeartbeatLoop.js';
16
17
  import { mintSession } from './sessionMint.js';
17
18
  import { parseIdentityResolveResponse } from '../auth/schemas.js';
19
+ import { staleNotificationSchema } from '../coordination/schema.js';
18
20
  /**
19
21
  * Interpret a heartbeat reply for a lease this handle HOLDS: anything other
20
22
  * than `held` means the lease is no longer ours (a holder cannot be `queued`;
@@ -33,7 +35,8 @@ function heldHeartbeatReply(reply, label) {
33
35
  }
34
36
  import { assertWriteOptions } from './writeOptionsSchema.js';
35
37
  import { createDurableHttpCommitEnvelope, canonicalHttpCommitBody, durableHttpCommitEnvelopeSchema, httpCommitEnvelopeRecordId, isHttpCommitReplayExpired, } from '../transactions/httpCommitEnvelope.js';
36
- /** Default per-request deadline for the stateless HTTP transport. */
38
+ import { resolveDurableWrites } from './durableWrites.js';
39
+ /** @internal Default per-request deadline for the private HTTP transport. */
37
40
  export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
38
41
  const successfulCommitResponseSchema = z
39
42
  .object({
@@ -45,6 +48,7 @@ const successfulCommitResponseSchema = z
45
48
  success: z.literal(true),
46
49
  lastSyncId: z.number().int().nonnegative().optional(),
47
50
  ops: z.number().int().positive(),
51
+ notifications: z.array(staleNotificationSchema).optional(),
48
52
  /** Ids of UPDATE/DELETE targets that matched zero rows (loud 0-row writes). */
49
53
  missingIds: z.array(z.string().min(1)).optional(),
50
54
  })
@@ -61,8 +65,30 @@ function parseSuccessfulCommitResponse(value, idempotencyKey) {
61
65
  }
62
66
  return parsed.data;
63
67
  }
64
- const DEFAULT_AGENT_LEASE = '10m';
65
- export function createProtocolClient(options) {
68
+ /** Decode the HTTP claim DTO into the one public Claim shape. */
69
+ function claimFromModelClaim(claim) {
70
+ return {
71
+ object: 'claim',
72
+ id: claim.id,
73
+ ...(claim.status ? { status: claim.status } : {}),
74
+ reason: claim.reason,
75
+ ...(claim.description ? { description: claim.description } : {}),
76
+ heldBy: claim.actor,
77
+ participantKind: claim.participantKind,
78
+ expiresAt: claim.expiresAt,
79
+ ...(claim.position !== undefined ? { position: claim.position } : {}),
80
+ target: {
81
+ type: claim.target.model,
82
+ id: claim.target.id,
83
+ ...(claim.target.path ? { path: claim.target.path } : {}),
84
+ ...(claim.target.range ? { range: claim.target.range } : {}),
85
+ ...(claim.target.field ? { field: claim.target.field } : {}),
86
+ ...(claim.target.meta ? { meta: claim.target.meta } : {}),
87
+ },
88
+ };
89
+ }
90
+ /** @internal Constructed only by the typed HTTP facade. */
91
+ export function createHttpTransport(options) {
66
92
  const env = readProcessEnv();
67
93
  const authInput = { options, env };
68
94
  const configuredApiKey = resolveApiKey(authInput);
@@ -123,7 +149,12 @@ export function createProtocolClient(options) {
123
149
  url,
124
150
  bootstrapBaseUrl: options.bootstrapBaseUrl,
125
151
  }).replace(/\/+$/, '');
126
- const commitOutbox = options.commitOutbox;
152
+ const durableWrites = resolveDurableWrites(options);
153
+ // Internal replay code retains transactional-outbox terminology. The public
154
+ // constructor exposes the behavior as `durableWrites`.
155
+ const commitOutbox = durableWrites.store;
156
+ const durableWriteNamespace = durableWrites.namespace ?? 'http';
157
+ const legacyCommitOutboxScope = options.commitOutboxScope;
127
158
  const httpOutboxPlaneNamespace = canonicalHttpCommitBody({
128
159
  apiBaseUrl,
129
160
  defaultQuery: Object.entries(options.defaultQuery ?? {}).sort(([a], [b]) => a.localeCompare(b)),
@@ -141,14 +172,19 @@ export function createProtocolClient(options) {
141
172
  return null;
142
173
  if (httpOutboxScopeNamespace)
143
174
  return httpOutboxScopeNamespace;
144
- let scope = options.commitOutboxScope;
175
+ let scope = legacyCommitOutboxScope
176
+ ? {
177
+ ...legacyCommitOutboxScope,
178
+ namespace: durableWriteNamespace,
179
+ }
180
+ : undefined;
145
181
  if (!scope) {
146
182
  const rawIdentity = await requestJson('/auth/identity', { method: 'GET' }, true);
147
183
  const identity = parseIdentityResolveResponse(rawIdentity);
148
184
  scope = {
149
185
  organizationId: identity.accountScope,
150
186
  participantId: identity.participantId,
151
- namespace: 'http',
187
+ namespace: durableWriteNamespace,
152
188
  };
153
189
  }
154
190
  httpOutboxScopeNamespace = canonicalHttpCommitBody({
@@ -180,11 +216,11 @@ export function createProtocolClient(options) {
180
216
  throw error;
181
217
  }
182
218
  }
183
- async function authHeaders() {
219
+ async function authHeaders(sealedProtocolVersion) {
184
220
  const apiKey = await resolveApiKeyValue(configuredApiKey);
185
221
  const token = apiKey ?? configuredAuthToken;
186
222
  if (!token) {
187
- throw new AbloAuthenticationError('Ablo({ apiKey }) requires an API key. Pass `apiKey` or set ABLO_API_KEY.', { code: 'api_key_required' });
223
+ throw new AbloAuthenticationError('The HTTP client requires an API key. Pass `apiKey` or set ABLO_API_KEY.', { code: 'api_key_required' });
188
224
  }
189
225
  const headers = {
190
226
  'Content-Type': 'application/json',
@@ -201,6 +237,12 @@ export function createProtocolClient(options) {
201
237
  headers[key] = value;
202
238
  }
203
239
  }
240
+ // A durable write owns its wire version. Force the sealed value after
241
+ // caller defaults so a restarted (or rolled-back) SDK cannot rewrite the
242
+ // protocol identity of a request that may already have reached the server.
243
+ if (sealedProtocolVersion !== undefined) {
244
+ headers[PROTOCOL_VERSION_HEADER] = String(sealedProtocolVersion);
245
+ }
204
246
  return headers;
205
247
  }
206
248
  function endpoint(path) {
@@ -215,8 +257,8 @@ export function createProtocolClient(options) {
215
257
  async function requestJson(path, init, skipReady = false) {
216
258
  if (!skipReady)
217
259
  await ready();
218
- const { idempotencyKey, ...requestInit } = init;
219
- const headers = await authHeaders();
260
+ const { idempotencyKey, sealedProtocolVersion, ...requestInit } = init;
261
+ const headers = await authHeaders(sealedProtocolVersion);
220
262
  if (idempotencyKey)
221
263
  headers['Idempotency-Key'] = idempotencyKey;
222
264
  // Deadline: abort the request after `timeoutMs` so a black-holed server
@@ -340,6 +382,7 @@ export function createProtocolClient(options) {
340
382
  const raw = await requestJson(envelope.request.path, {
341
383
  method: envelope.request.method,
342
384
  idempotencyKey: envelope.idempotencyKey,
385
+ sealedProtocolVersion: envelope.protocolVersion,
343
386
  body: envelope.request.body,
344
387
  }, true);
345
388
  const response = parseSuccessfulCommitResponse(raw, envelope.idempotencyKey);
@@ -366,7 +409,7 @@ export function createProtocolClient(options) {
366
409
  return null;
367
410
  const scopeNamespace = await resolveHttpOutboxScope();
368
411
  if (!scopeNamespace) {
369
- throw new AbloValidationError('HTTP commit outbox scope was not resolved', {
412
+ throw new AbloValidationError('HTTP durable-write scope was not resolved', {
370
413
  code: 'write_options_invalid',
371
414
  });
372
415
  }
@@ -424,6 +467,9 @@ export function createProtocolClient(options) {
424
467
  const raw = await requestJson(input.path, {
425
468
  method: input.method,
426
469
  idempotencyKey: input.idempotencyKey,
470
+ ...(durableEnvelope
471
+ ? { sealedProtocolVersion: durableEnvelope.protocolVersion }
472
+ : {}),
427
473
  body: requestBody,
428
474
  }, true);
429
475
  response = parseSuccessfulCommitResponse(raw, input.idempotencyKey);
@@ -462,32 +508,11 @@ export function createProtocolClient(options) {
462
508
  ? crypto.randomUUID()
463
509
  : `id_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
464
510
  }
465
- function childClient(authToken, scope) {
466
- return createProtocolClient({
467
- ...options,
468
- apiKey: null,
469
- authToken,
470
- schema: null,
471
- ...(scope
472
- ? {
473
- commitOutboxScope: {
474
- ...scope,
475
- namespace: options.commitOutboxScope?.namespace ?? 'http',
476
- },
477
- }
478
- : { commitOutboxScope: undefined }),
479
- });
480
- }
481
511
  function normalizeCommitOperation(op, defaults) {
482
- const model = op.model ?? op.target?.model;
483
- if (!model) {
484
- throw new AbloValidationError('Commit operation requires `model` or `target.model`.', { code: 'commit_operation_model_required' });
485
- }
486
- const id = op.id ?? op.target?.id ?? null;
487
512
  return {
488
513
  action: op.action,
489
- model,
490
- id,
514
+ model: op.model,
515
+ id: op.id ?? null,
491
516
  data: op.data ?? null,
492
517
  transactionId: op.transactionId ?? null,
493
518
  readAt: op.readAt ?? defaults.readAt ?? null,
@@ -495,20 +520,10 @@ export function createProtocolClient(options) {
495
520
  };
496
521
  }
497
522
  function normalizeCommitOperations(commitOptions) {
498
- if (commitOptions.operation && commitOptions.operations) {
499
- throw new AbloValidationError('Pass either `operation` or `operations`, not both.', { code: 'commit_operations_ambiguous' });
500
- }
501
- const inputOperations = commitOptions.operation
502
- ? [commitOptions.operation]
503
- : commitOptions.operations ?? [];
504
- if (inputOperations.length === 0) {
505
- throw new AbloValidationError('Commit requires at least one operation.', { code: 'commit_operation_required' });
523
+ if (commitOptions.operations.length === 0) {
524
+ throw new AbloValidationError('Commit requires a non-empty `operations` array.', { code: 'commit_operation_required' });
506
525
  }
507
- return inputOperations.map((op) => normalizeCommitOperation(op, commitOptions));
508
- }
509
- async function listClaims(target) {
510
- const state = await listClaimState(target);
511
- return state.active;
526
+ return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions));
512
527
  }
513
528
  async function listClaimState(target) {
514
529
  const params = new URLSearchParams();
@@ -525,54 +540,6 @@ export function createProtocolClient(options) {
525
540
  queue: body.queue ?? [],
526
541
  };
527
542
  }
528
- function delay(ms, signal) {
529
- if (signal?.aborted) {
530
- return Promise.reject(new AbloConnectionError('Claim wait aborted.', {
531
- code: 'claim_wait_aborted',
532
- cause: signal.reason,
533
- }));
534
- }
535
- return new Promise((resolve, reject) => {
536
- const timeout = setTimeout(done, ms);
537
- function cleanup() {
538
- clearTimeout(timeout);
539
- signal?.removeEventListener('abort', onAbort);
540
- }
541
- function done() {
542
- cleanup();
543
- resolve();
544
- }
545
- function onAbort() {
546
- cleanup();
547
- reject(new AbloConnectionError('Claim wait aborted.', {
548
- code: 'claim_wait_aborted',
549
- cause: signal?.reason,
550
- }));
551
- }
552
- signal?.addEventListener('abort', onAbort, { once: true });
553
- });
554
- }
555
- async function waitForNoClaims(target, options) {
556
- const startedAt = Date.now();
557
- const pollInterval = options?.pollInterval;
558
- for (;;) {
559
- const claims = await listClaims(target);
560
- if (claims.length === 0)
561
- return;
562
- if (pollInterval == null) {
563
- throw new AbloValidationError('Cannot wait for claims over the HTTP client without `pollInterval`. ' +
564
- 'Use the schema client for event-driven claim waits, pass `ifClaimed: "return"`, ' +
565
- 'or provide an explicit poll interval for this runtime.', { code: 'claim_wait_poll_interval_required' });
566
- }
567
- if (options?.timeout != null && Date.now() - startedAt >= options.timeout) {
568
- throw claimedError(target, claims, 'model_claimed_timeout');
569
- }
570
- const remaining = options?.timeout == null
571
- ? pollInterval
572
- : Math.max(0, Math.min(pollInterval, options.timeout - (Date.now() - startedAt)));
573
- await delay(remaining, options?.signal);
574
- }
575
- }
576
543
  async function applyClaimedPolicy(target, options, defaultPolicy = 'return') {
577
544
  const policy = options?.ifClaimed ?? defaultPolicy;
578
545
  if (policy === 'return')
@@ -603,9 +570,6 @@ export function createProtocolClient(options) {
603
570
  onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
604
571
  });
605
572
  const requestBody = {
606
- clientTxId,
607
- idempotencyKey: clientTxId,
608
- claim: normalizeClaimId(commitOptions.claimRef) ?? claim?.id,
609
573
  operations,
610
574
  reads: commitOptions.reads,
611
575
  };
@@ -637,195 +601,15 @@ export function createProtocolClient(options) {
637
601
  id: body.id ?? body.clientTxId,
638
602
  status,
639
603
  lastSyncId: body.lastSyncId,
604
+ ...(body.notifications && body.notifications.length > 0
605
+ ? { notifications: body.notifications }
606
+ : {}),
640
607
  ...(body.missingIds && body.missingIds.length > 0
641
608
  ? { missingIds: body.missingIds }
642
609
  : {}),
643
610
  };
644
611
  },
645
612
  };
646
- const capabilities = {
647
- async create(capabilityOptions) {
648
- const ttlSeconds = capabilityOptions.ttlSeconds ?? // eslint-disable-line @typescript-eslint/no-deprecated -- compatibility alias
649
- capabilityOptions.leaseSeconds ??
650
- toSeconds(capabilityOptions.ttl ?? capabilityOptions.lease ?? DEFAULT_AGENT_LEASE); // eslint-disable-line @typescript-eslint/no-deprecated -- compatibility alias
651
- const body = await requestJson('/v1/capabilities', {
652
- method: 'POST',
653
- body: JSON.stringify({
654
- participantKind: capabilityOptions.participantKind ?? 'agent',
655
- participantId: capabilityOptions.participantId,
656
- syncGroups: capabilityOptions.syncGroups,
657
- operations: capabilityOptions.operations,
658
- ttlSeconds,
659
- label: capabilityOptions.label,
660
- wideScope: capabilityOptions.wideScope,
661
- userMeta: capabilityOptions.userMeta,
662
- }),
663
- });
664
- const id = body.capabilityId ?? body.id;
665
- if (!id) {
666
- throw new AbloValidationError('Capability create response did not include an id.', { code: 'capability_id_missing' });
667
- }
668
- return {
669
- id,
670
- token: body.token,
671
- expiresAt: body.expiresAt,
672
- organizationId: body.organizationId,
673
- scope: body.scope,
674
- userMeta: body.userMeta,
675
- client: () => childClient(body.token, {
676
- organizationId: body.organizationId,
677
- participantId: body.scope.participantId,
678
- }),
679
- };
680
- },
681
- async retrieve(id) {
682
- const body = await requestJson(`/v1/capabilities/${encodeURIComponent(id)}`, { method: 'GET' });
683
- return {
684
- id: body.capabilityId ?? body.id ?? id,
685
- organizationId: body.organizationId,
686
- participantKind: body.participantKind,
687
- participantId: body.participantId,
688
- label: body.label,
689
- status: body.status,
690
- issuedAt: body.issuedAt,
691
- expiresAt: body.expiresAt,
692
- revokedAt: body.revokedAt,
693
- lastUsedAt: body.lastUsedAt,
694
- operations: body.operations ?? [],
695
- syncGroups: body.syncGroups ?? [],
696
- };
697
- },
698
- async revoke(id) {
699
- const body = await requestJson(`/v1/capabilities/${encodeURIComponent(id)}`, { method: 'DELETE' });
700
- return {
701
- id: body.capabilityId ?? body.id ?? id,
702
- deleted: body.deleted ?? true,
703
- activeSessionsClosed: body.activeSessionsClosed,
704
- };
705
- },
706
- async rotate(id, rotateOptions = {}) {
707
- const graceSeconds = rotateOptions.graceSeconds ??
708
- (rotateOptions.grace !== undefined ? toSeconds(rotateOptions.grace) : undefined);
709
- const leaseSeconds = rotateOptions.leaseSeconds ??
710
- (rotateOptions.lease !== undefined ? toSeconds(rotateOptions.lease) : undefined);
711
- const body = await requestJson(`/v1/capabilities/${encodeURIComponent(id)}/rotate`, {
712
- method: 'POST',
713
- body: JSON.stringify({
714
- ...(graceSeconds !== undefined ? { graceSeconds } : {}),
715
- ...(leaseSeconds !== undefined ? { ttlSeconds: leaseSeconds } : {}),
716
- }),
717
- });
718
- const newId = body.capabilityId ?? body.id;
719
- if (!newId) {
720
- throw new AbloValidationError('Capability rotate response did not include an id.', { code: 'capability_id_missing' });
721
- }
722
- return {
723
- id: newId,
724
- token: body.token,
725
- expiresAt: body.expiresAt,
726
- organizationId: body.organizationId,
727
- scope: body.scope,
728
- rotatedFrom: {
729
- id: body.rotatedFrom.capabilityId ?? body.rotatedFrom.id ?? id,
730
- expiresAt: body.rotatedFrom.expiresAt,
731
- },
732
- client: () => childClient(body.token, {
733
- organizationId: body.organizationId,
734
- participantId: body.scope.participantId,
735
- }),
736
- };
737
- },
738
- mint(options) {
739
- return capabilities.create(options);
740
- },
741
- };
742
- const claims = {
743
- async create(claimOptions) {
744
- const claimId = createClaimId();
745
- const body = await requestJson('/v1/claims', {
746
- method: 'POST',
747
- body: JSON.stringify({
748
- claimId,
749
- target: claimOptions.target,
750
- reason: claimOptions.reason,
751
- ttl: claimOptions.ttl,
752
- queue: claimOptions.queue,
753
- }),
754
- });
755
- // The fair-queue grant is pushed over a WebSocket (`claim_granted`), which
756
- // this stateless HTTP client doesn't hold. Returning a handle here would be
757
- // a phantom holder — a lease we can't confirm is ours. So a queued response
758
- // is surfaced as a typed claimed signal; callers that need to wait in line
759
- // use the realtime (WebSocket-backed) `ablo.<model>.claim`.
760
- if (body.status === 'queued') {
761
- throw new AbloClaimedError(`Target ${claimOptions.target.model}/${claimOptions.target.id} is held; ` +
762
- `queued at position ${body.position ?? 0}. The HTTP client can't await ` +
763
- `the grant (no socket) — use the realtime client to wait in line.`, { code: 'claim_queued' });
764
- }
765
- const id = body.claim?.id ?? claimId;
766
- let released = false;
767
- const release = async () => {
768
- if (released)
769
- return;
770
- released = true;
771
- await requestJson(`/v1/claims/${encodeURIComponent(id)}`, { method: 'DELETE' });
772
- };
773
- // The by-id twin of the model-scoped heartbeat — same reply contract.
774
- const heartbeat = async (beatOptions) => {
775
- const resolved = resolveHeartbeatOptions(beatOptions);
776
- const reply = await requestJson(`/v1/claims/${encodeURIComponent(id)}/heartbeat`, {
777
- method: 'POST',
778
- body: JSON.stringify({
779
- ...(resolved.ttl !== undefined ? { ttl: resolved.ttl } : {}),
780
- ...(resolved.details !== undefined
781
- ? { details: resolved.details }
782
- : {}),
783
- }),
784
- });
785
- return heldHeartbeatReply(reply, `claim ${id}`);
786
- };
787
- return {
788
- object: 'claim',
789
- id,
790
- heartbeat,
791
- reason: claimOptions.reason,
792
- target: {
793
- type: claimOptions.target.model,
794
- id: claimOptions.target.id,
795
- ...(claimOptions.target.field ? { field: claimOptions.target.field } : {}),
796
- ...(claimOptions.target.path ? { path: claimOptions.target.path } : {}),
797
- ...(claimOptions.target.range ? { range: claimOptions.target.range } : {}),
798
- ...(claimOptions.target.meta ? { meta: claimOptions.target.meta } : {}),
799
- },
800
- release,
801
- revoke: () => {
802
- void release().catch(() => { });
803
- },
804
- [Symbol.asyncDispose]: release,
805
- };
806
- },
807
- list: listClaims,
808
- waitFor(target, options) {
809
- return waitForNoClaims(target, options);
810
- },
811
- async heartbeatAll(options) {
812
- const reply = await requestJson('/v1/claims/heartbeat', {
813
- method: 'POST',
814
- body: JSON.stringify(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
815
- });
816
- return (reply.results ?? []).flatMap((entry) => typeof entry.claimId === 'string' && typeof entry.expiresAt === 'number'
817
- ? [
818
- {
819
- claimId: entry.claimId,
820
- expiresAt: entry.expiresAt,
821
- ...(entry.queueDepth !== undefined
822
- ? { queueDepth: entry.queueDepth }
823
- : {}),
824
- },
825
- ]
826
- : []);
827
- },
828
- };
829
613
  async function listModel(modelName, options) {
830
614
  const params = new URLSearchParams();
831
615
  if (options?.limit !== undefined)
@@ -906,7 +690,6 @@ export function createProtocolClient(options) {
906
690
  : undefined;
907
691
  const readAt = options?.readAt ?? claimHandle?.readAt;
908
692
  const requestBody = {
909
- idempotencyKey: clientTxId,
910
693
  claim: normalizeClaimId(options?.claimRef) ?? claimHandle?.id,
911
694
  onStale: options?.onStale ?? (claimHandle?.readAt !== undefined ? 'reject' : undefined),
912
695
  readAt,
@@ -1064,11 +847,14 @@ export function createProtocolClient(options) {
1064
847
  state: async (params) => {
1065
848
  const res = await claimsForEntity(params);
1066
849
  const first = res.claims?.[0];
1067
- return first ?? null;
850
+ return first ? claimFromModelClaim(first) : null;
1068
851
  },
1069
852
  queue: async (params) => {
1070
853
  const res = await claimsForEntity(params);
1071
- return { object: 'list', data: res.queue ?? [] };
854
+ return {
855
+ object: 'list',
856
+ data: (res.queue ?? []).map(claimFromModelClaim),
857
+ };
1072
858
  },
1073
859
  reorder: async (params) => {
1074
860
  await requestJson(`${claimPath(params.id)}/reorder`, {
@@ -1184,8 +970,6 @@ export function createProtocolClient(options) {
1184
970
  }),
1185
971
  async dispose() { },
1186
972
  async purge() { },
1187
- capabilities,
1188
- claims,
1189
973
  commits,
1190
974
  model,
1191
975
  sessions: {
@@ -1201,6 +985,9 @@ export function createProtocolClient(options) {
1201
985
  apiKey,
1202
986
  baseUrl: apiBaseUrl,
1203
987
  ...(options.fetch ? { fetch: options.fetch } : {}),
988
+ ...(options.modelTypenames
989
+ ? { modelTypenames: options.modelTypenames }
990
+ : {}),
1204
991
  });
1205
992
  },
1206
993
  },
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * The option types for the {@link Ablo} client: the public {@link AbloOptions}
3
- * bag callers pass, and the fuller {@link InternalAbloOptions} construction
4
- * surface. This module holds only types and has no runtime imports.
3
+ * bag callers pass, and the fuller monorepo-only {@link InternalAbloOptions}
4
+ * construction surface exposed through `@abloatai/ablo/core`.
5
+ * This module holds only types and has no runtime imports.
5
6
  */
6
7
  import type { Schema, SchemaRecord } from '../schema/schema.js';
7
8
  import type { SyncEngineConfig, SyncLogger, MutationExecutor, SyncObservabilityProvider, SyncAnalytics, SessionErrorDetector, OnlineStatusProvider } from '../interfaces/index.js';
8
9
  import type { AbloPersistence } from './persistence.js';
9
- import type { CommitOutboxStore } from '../transactions/commitOutboxStore.js';
10
+ import type { DurableWriteStore, DurableWritesConfig } from '../transactions/durableWriteStore.js';
10
11
  import type { CommitOutboxScope } from '../transactions/commitEnvelope.js';
11
12
  /**
12
13
  * An async function that resolves an apiKey at request time. Use it for credential
@@ -111,16 +112,38 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
111
112
  */
112
113
  persistence?: AbloPersistence;
113
114
  /**
114
- * Durable commit-envelope storage for server-side agents and workers.
115
- * The stateful browser client uses its strict IndexedDB transaction store;
116
- * the stateless HTTP client has no implicit filesystem and therefore needs a
117
- * workflow-, SQLite-, or filesystem-backed implementation to survive a
118
- * process restart. One store may serve several actors because every record
119
- * is checked against its authenticated scope before replay.
115
+ * Makes `create`, `update`, and `delete` survive process restarts and
116
+ * ambiguous network responses. Server-side agents and workers inject a
117
+ * workflow-, SQLite-, or filesystem-backed store; authenticated identity is
118
+ * used to fence replay to the actor that originally made the write.
119
+ *
120
+ * Most clients leave this unset and use Ablo's default in-memory cache.
121
+ * `durableWrites` is independent of that cache: it is the optional outbound
122
+ * write journal for workers that must recover automatically after a crash.
123
+ * Browsers only use IndexedDB when `persistence: 'indexeddb'` is explicitly
124
+ * enabled for offline/reload behavior.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * Ablo({
129
+ * schema,
130
+ * apiKey,
131
+ * durableWrites: { store, namespace: 'agent-worker' },
132
+ * })
133
+ * ```
120
134
  */
121
- commitOutbox?: CommitOutboxStore;
135
+ durableWrites?: DurableWritesConfig;
122
136
  /**
123
- * Stable actor/server scope for an injected HTTP {@link commitOutbox}.
137
+ * @deprecated Use `durableWrites: { store, namespace }`.
138
+ * Retained as a compatibility alias through the next major release.
139
+ */
140
+ commitOutbox?: DurableWriteStore;
141
+ /**
142
+ * @deprecated Actor identity is resolved from authentication. Use
143
+ * `durableWrites.namespace` only when shared storage needs separate workflow
144
+ * or deployment lanes.
145
+ *
146
+ * Stable actor/server scope for an injected HTTP `commitOutbox`.
124
147
  * Omit it to resolve `organizationId` and `participantId` from the authenticated
125
148
  * `/auth/identity` endpoint. Supplying it avoids that one setup request in
126
149
  * trusted worker environments; `namespace` distinguishes deployments or
@@ -137,8 +160,7 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
137
160
  * {@link AbloHttpClient}, so stateful-only capabilities such as `get`, `getAll`,
138
161
  * and `onChange` become compile errors instead of runtime gaps.
139
162
  *
140
- * Note: session minting through `sessions.create` runs on the default WebSocket
141
- * client, not the HTTP client.
163
+ * Session minting through `sessions.create` is available on both transports.
142
164
  *
143
165
  * @default 'websocket'
144
166
  */
@@ -241,9 +263,9 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
241
263
  /**
242
264
  * TypeScript schema defined with `defineSchema()`.
243
265
  *
244
- * The root `Ablo(...)` client is schema-first so consumers get typed
245
- * model clients such as `ablo.weatherReports.update(...)`. Omit `schema`
246
- * only for the advanced Model / Claim / Commit client.
266
+ * Required on every public `Ablo(...)` construction. It produces typed model
267
+ * clients such as `ablo.weatherReports.update(...)`; schema-agnostic routing
268
+ * exists only inside the private HTTP transport.
247
269
  */
248
270
  schema: Schema<S>;
249
271
  /**
@@ -299,9 +321,11 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
299
321
  * reload-surviving local cache in a browser.
300
322
  */
301
323
  persistence?: AbloPersistence;
302
- /** Internal mirror of {@link AbloOptions.commitOutbox}. */
303
- commitOutbox?: CommitOutboxStore;
304
- /** Internal mirror of {@link AbloOptions.commitOutboxScope}. */
324
+ /** Internal mirror of {@link AbloOptions.durableWrites}. */
325
+ durableWrites?: DurableWritesConfig;
326
+ /** @deprecated Internal mirror of {@link AbloOptions.commitOutbox}. */
327
+ commitOutbox?: DurableWriteStore;
328
+ /** @deprecated Internal mirror of {@link AbloOptions.commitOutboxScope}. */
305
329
  commitOutboxScope?: CommitOutboxScope;
306
330
  /** @deprecated Use `persistence: 'indexeddb'` for durable browser storage. */
307
331
  offline?: boolean;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * The option types for the {@link Ablo} client: the public {@link AbloOptions}
3
- * bag callers pass, and the fuller {@link InternalAbloOptions} construction
4
- * surface. This module holds only types and has no runtime imports.
3
+ * bag callers pass, and the fuller monorepo-only {@link InternalAbloOptions}
4
+ * construction surface exposed through `@abloatai/ablo/core`.
5
+ * This module holds only types and has no runtime imports.
5
6
  */
6
7
  export {};