@abloatai/ablo 0.10.1 → 0.11.1

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 (105) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +63 -23
  3. package/dist/BaseSyncedStore.d.ts +75 -0
  4. package/dist/BaseSyncedStore.js +193 -8
  5. package/dist/Database.d.ts +10 -2
  6. package/dist/Database.js +15 -1
  7. package/dist/SyncClient.d.ts +12 -1
  8. package/dist/SyncClient.js +110 -26
  9. package/dist/agent/Agent.d.ts +9 -9
  10. package/dist/agent/Agent.js +16 -16
  11. package/dist/agent/index.d.ts +1 -1
  12. package/dist/agent/index.js +2 -2
  13. package/dist/agent/types.d.ts +1 -1
  14. package/dist/agent/types.js +1 -1
  15. package/dist/ai-sdk/{intent-broadcast.d.ts → claim-broadcast.d.ts} +10 -10
  16. package/dist/ai-sdk/{intent-broadcast.js → claim-broadcast.js} +6 -6
  17. package/dist/ai-sdk/coordination-context.d.ts +9 -9
  18. package/dist/ai-sdk/coordination-context.js +8 -8
  19. package/dist/ai-sdk/index.d.ts +1 -1
  20. package/dist/ai-sdk/index.js +1 -1
  21. package/dist/ai-sdk/wrap.d.ts +4 -4
  22. package/dist/ai-sdk/wrap.js +4 -4
  23. package/dist/api/index.d.ts +2 -2
  24. package/dist/cli.cjs +369 -67
  25. package/dist/client/Ablo.d.ts +30 -63
  26. package/dist/client/Ablo.js +124 -103
  27. package/dist/client/ApiClient.d.ts +6 -5
  28. package/dist/client/ApiClient.js +86 -62
  29. package/dist/client/auth.d.ts +9 -4
  30. package/dist/client/auth.js +40 -5
  31. package/dist/client/createModelProxy.d.ts +41 -54
  32. package/dist/client/createModelProxy.js +123 -20
  33. package/dist/client/httpClient.d.ts +2 -0
  34. package/dist/client/httpClient.js +1 -1
  35. package/dist/client/index.d.ts +3 -3
  36. package/dist/client/writeOptionsSchema.d.ts +4 -4
  37. package/dist/client/writeOptionsSchema.js +4 -4
  38. package/dist/coordination/schema.d.ts +249 -38
  39. package/dist/coordination/schema.js +172 -39
  40. package/dist/core/index.d.ts +2 -2
  41. package/dist/core/index.js +4 -4
  42. package/dist/errorCodes.d.ts +9 -9
  43. package/dist/errorCodes.js +16 -16
  44. package/dist/errors.d.ts +51 -2
  45. package/dist/errors.js +94 -5
  46. package/dist/interfaces/index.d.ts +8 -4
  47. package/dist/policy/index.d.ts +1 -1
  48. package/dist/policy/types.d.ts +13 -13
  49. package/dist/policy/types.js +8 -8
  50. package/dist/react/AbloProvider.d.ts +51 -4
  51. package/dist/react/AbloProvider.js +95 -11
  52. package/dist/react/context.d.ts +26 -9
  53. package/dist/react/context.js +2 -2
  54. package/dist/react/index.d.ts +4 -4
  55. package/dist/react/index.js +4 -4
  56. package/dist/react/useAblo.js +5 -5
  57. package/dist/react/{useIntent.d.ts → useClaim.d.ts} +9 -9
  58. package/dist/react/useClaim.js +42 -0
  59. package/dist/schema/index.js +1 -1
  60. package/dist/schema/schema.d.ts +3 -3
  61. package/dist/schema/sugar.d.ts +3 -3
  62. package/dist/schema/sugar.js +3 -3
  63. package/dist/schema/sync-delta-wire.d.ts +8 -8
  64. package/dist/server/commit.d.ts +2 -2
  65. package/dist/sync/AreaOfInterestManager.d.ts +162 -0
  66. package/dist/sync/AreaOfInterestManager.js +233 -0
  67. package/dist/sync/BootstrapHelper.d.ts +9 -1
  68. package/dist/sync/BootstrapHelper.js +15 -5
  69. package/dist/sync/NetworkProbe.d.ts +1 -1
  70. package/dist/sync/NetworkProbe.js +1 -1
  71. package/dist/sync/SyncWebSocket.d.ts +59 -25
  72. package/dist/sync/SyncWebSocket.js +123 -26
  73. package/dist/sync/awaitClaimGrant.d.ts +40 -0
  74. package/dist/sync/awaitClaimGrant.js +86 -0
  75. package/dist/sync/createClaimStream.d.ts +34 -0
  76. package/dist/sync/{createIntentStream.js → createClaimStream.js} +92 -81
  77. package/dist/sync/createPresenceStream.js +3 -2
  78. package/dist/sync/participants.d.ts +10 -10
  79. package/dist/sync/participants.js +17 -10
  80. package/dist/sync/schemas.d.ts +8 -8
  81. package/dist/transactions/TransactionQueue.d.ts +23 -0
  82. package/dist/transactions/TransactionQueue.js +186 -12
  83. package/dist/types/global.d.ts +18 -13
  84. package/dist/types/global.js +11 -6
  85. package/dist/types/index.d.ts +9 -7
  86. package/dist/types/index.js +2 -2
  87. package/dist/types/streams.d.ts +114 -98
  88. package/dist/types/streams.js +1 -1
  89. package/dist/utils/asyncIterator.d.ts +1 -1
  90. package/dist/utils/asyncIterator.js +1 -1
  91. package/dist/wire/frames.d.ts +2 -2
  92. package/docs/api.md +3 -3
  93. package/docs/client-behavior.md +6 -3
  94. package/docs/coordination.md +13 -3
  95. package/docs/data-sources.md +29 -9
  96. package/docs/migration.md +40 -0
  97. package/docs/quickstart.md +61 -33
  98. package/docs/react.md +46 -0
  99. package/llms-full.txt +25 -8
  100. package/llms.txt +11 -9
  101. package/package.json +3 -2
  102. package/dist/react/useIntent.js +0 -42
  103. package/dist/sync/awaitIntentGrant.d.ts +0 -40
  104. package/dist/sync/awaitIntentGrant.js +0 -62
  105. package/dist/sync/createIntentStream.d.ts +0 -34
@@ -5,8 +5,9 @@
5
5
  * IndexedDB, no WebSocket. It maps the public Model / Claim / Commit
6
6
  * nouns directly to HTTP routes on sync-server.
7
7
  */
8
- import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError, } from '../errors.js';
9
- import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, } from './auth.js';
8
+ import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, claimedError, translateHttpError, } from '../errors.js';
9
+ import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfDatabaseUrlEnvIgnored, } from './auth.js';
10
+ import { registerDataSource } from './registerDataSource.js';
10
11
  import { toSeconds } from '../utils/duration.js';
11
12
  import { assertWriteOptions } from './writeOptionsSchema.js';
12
13
  const DEFAULT_AGENT_LEASE = '10m';
@@ -15,8 +16,13 @@ export function createProtocolClient(options) {
15
16
  const authInput = { options, env };
16
17
  const configuredApiKey = resolveApiKey(authInput);
17
18
  const configuredAuthToken = resolveAuthToken(authInput);
19
+ const configuredDatabaseUrl = resolveDatabaseUrl(authInput);
20
+ // Nudge (once) if a stray DATABASE_URL is in the env but `databaseUrl` wasn't
21
+ // passed — no logger on this path, so the helper falls back to console.warn.
22
+ warnIfDatabaseUrlEnvIgnored(authInput);
18
23
  assertBrowserSafety({
19
24
  apiKey: configuredApiKey,
25
+ databaseUrl: configuredDatabaseUrl,
20
26
  dangerouslyAllowBrowser: options.dangerouslyAllowBrowser,
21
27
  });
22
28
  const fetchImpl = options.fetch ?? globalThis.fetch;
@@ -28,6 +34,28 @@ export function createProtocolClient(options) {
28
34
  url,
29
35
  bootstrapBaseUrl: options.bootstrapBaseUrl,
30
36
  }).replace(/\/+$/, '');
37
+ let readyPromise = null;
38
+ async function ready() {
39
+ if (readyPromise)
40
+ return readyPromise;
41
+ readyPromise = (async () => {
42
+ if (!configuredDatabaseUrl)
43
+ return;
44
+ await registerDataSource({
45
+ baseUrl: apiBaseUrl,
46
+ apiKey: await resolveApiKeyValue(configuredApiKey),
47
+ databaseUrl: configuredDatabaseUrl,
48
+ ...(options.fetch ? { fetchImpl: options.fetch } : {}),
49
+ });
50
+ })();
51
+ try {
52
+ await readyPromise;
53
+ }
54
+ catch (error) {
55
+ readyPromise = null;
56
+ throw error;
57
+ }
58
+ }
31
59
  async function authHeaders() {
32
60
  const apiKey = await resolveApiKeyValue(configuredApiKey);
33
61
  const token = apiKey ?? configuredAuthToken;
@@ -57,6 +85,7 @@ export function createProtocolClient(options) {
57
85
  return target.toString();
58
86
  }
59
87
  async function requestJson(path, init) {
88
+ await ready();
60
89
  const { idempotencyKey, ...requestInit } = init;
61
90
  const headers = await authHeaders();
62
91
  if (idempotencyKey)
@@ -82,7 +111,7 @@ export function createProtocolClient(options) {
82
111
  ? crypto.randomUUID()
83
112
  : `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
84
113
  }
85
- function createIntentId() {
114
+ function createClaimId() {
86
115
  return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
87
116
  ? `int_${crypto.randomUUID()}`
88
117
  : `int_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
@@ -128,7 +157,7 @@ export function createProtocolClient(options) {
128
157
  }
129
158
  return inputOperations.map((op) => normalizeCommitOperation(op, commitOptions));
130
159
  }
131
- async function listIntents(target) {
160
+ async function listClaims(target) {
132
161
  const state = await listClaimState(target);
133
162
  return state.active;
134
163
  }
@@ -141,24 +170,16 @@ export function createProtocolClient(options) {
141
170
  if (target?.field)
142
171
  params.set('field', target.field);
143
172
  const suffix = params.toString();
144
- const body = await requestJson(`/v1/intents${suffix ? `?${suffix}` : ''}`, { method: 'GET' });
173
+ const body = await requestJson(`/v1/claims${suffix ? `?${suffix}` : ''}`, { method: 'GET' });
145
174
  return {
146
- active: body.intents ?? [],
175
+ active: body.claims ?? [],
147
176
  queue: body.queue ?? [],
148
177
  };
149
178
  }
150
- function claimedError(target, claims, code) {
151
- const label = [target.model, target.id, target.field].filter(Boolean).join('/');
152
- const holder = claims[0];
153
- const suffix = holder
154
- ? ` held by ${holder.actor} (${holder.action})`
155
- : ' held by another participant';
156
- return new AbloClaimedError(`Model row is claimed: ${label || 'target'}${suffix}.`, { code, claims });
157
- }
158
179
  function delay(ms, signal) {
159
180
  if (signal?.aborted) {
160
- return Promise.reject(new AbloConnectionError('Intent wait aborted.', {
161
- code: 'intent_wait_aborted',
181
+ return Promise.reject(new AbloConnectionError('Claim wait aborted.', {
182
+ code: 'claim_wait_aborted',
162
183
  cause: signal.reason,
163
184
  }));
164
185
  }
@@ -174,28 +195,28 @@ export function createProtocolClient(options) {
174
195
  }
175
196
  function onAbort() {
176
197
  cleanup();
177
- reject(new AbloConnectionError('Intent wait aborted.', {
178
- code: 'intent_wait_aborted',
198
+ reject(new AbloConnectionError('Claim wait aborted.', {
199
+ code: 'claim_wait_aborted',
179
200
  cause: signal?.reason,
180
201
  }));
181
202
  }
182
203
  signal?.addEventListener('abort', onAbort, { once: true });
183
204
  });
184
205
  }
185
- async function waitForNoIntents(target, options) {
206
+ async function waitForNoClaims(target, options) {
186
207
  const startedAt = Date.now();
187
208
  const pollInterval = options?.pollInterval;
188
209
  for (;;) {
189
- const intents = await listIntents(target);
190
- if (intents.length === 0)
210
+ const claims = await listClaims(target);
211
+ if (claims.length === 0)
191
212
  return;
192
213
  if (pollInterval == null) {
193
214
  throw new AbloValidationError('Cannot wait for claims over the HTTP client without `pollInterval`. ' +
194
215
  'Use the schema client for event-driven claim waits, pass `ifClaimed: "return"`, ' +
195
- 'or provide an explicit poll interval for this runtime.', { code: 'intent_wait_poll_interval_required' });
216
+ 'or provide an explicit poll interval for this runtime.', { code: 'claim_wait_poll_interval_required' });
196
217
  }
197
218
  if (options?.timeout != null && Date.now() - startedAt >= options.timeout) {
198
- throw claimedError(target, intents, 'model_claimed_timeout');
219
+ throw claimedError(target, claims, 'model_claimed_timeout');
199
220
  }
200
221
  const remaining = options?.timeout == null
201
222
  ? pollInterval
@@ -217,7 +238,7 @@ export function createProtocolClient(options) {
217
238
  state.queue.length >= options.maxQueueDepth) {
218
239
  throw claimedError(target, state.active, 'queue_too_deep');
219
240
  }
220
- await waitForNoIntents(target, {
241
+ await waitForNoClaims(target, {
221
242
  timeout: options?.claimedTimeout,
222
243
  pollInterval: options?.claimedPollInterval,
223
244
  });
@@ -230,7 +251,7 @@ export function createProtocolClient(options) {
230
251
  readAt: commitOptions.readAt,
231
252
  onStale: commitOptions.onStale,
232
253
  wait: commitOptions.wait,
233
- intent: commitOptions.intent,
254
+ claim: commitOptions.claim,
234
255
  }, 'commits.create');
235
256
  const clientTxId = createClientTxId(commitOptions.idempotencyKey);
236
257
  // Same claim vocabulary as the WS client's `commits.create`: a handle
@@ -247,7 +268,7 @@ export function createProtocolClient(options) {
247
268
  body: JSON.stringify({
248
269
  clientTxId,
249
270
  idempotencyKey: clientTxId,
250
- intent: normalizeIntentId(commitOptions.intent) ?? claim?.claimId,
271
+ claim: normalizeClaimId(commitOptions.claimRef) ?? claim?.claimId,
251
272
  operations,
252
273
  }),
253
274
  });
@@ -353,39 +374,42 @@ export function createProtocolClient(options) {
353
374
  return capabilities.create(options);
354
375
  },
355
376
  };
356
- const intents = {
357
- async create(intentOptions) {
358
- const intentId = createIntentId();
359
- const body = await requestJson('/v1/intents', {
377
+ const claims = {
378
+ async create(claimOptions) {
379
+ const claimId = createClaimId();
380
+ const body = await requestJson('/v1/claims', {
360
381
  method: 'POST',
361
382
  body: JSON.stringify({
362
- intentId,
363
- target: intentOptions.target,
364
- action: intentOptions.action,
365
- ttl: intentOptions.ttl,
366
- queue: intentOptions.queue,
383
+ claimId,
384
+ target: claimOptions.target,
385
+ action: claimOptions.action,
386
+ ttl: claimOptions.ttl,
387
+ queue: claimOptions.queue,
367
388
  }),
368
389
  });
369
- // The fair-queue grant is PUSHED over a WebSocket (`intent_granted`),
390
+ // The fair-queue grant is PUSHED over a WebSocket (`claim_granted`),
370
391
  // which this stateless HTTP client doesn't hold. Returning a handle here
371
392
  // would be a phantom holder — a lease we can't confirm is ours. So a
372
393
  // queued response is surfaced as a typed claimed signal; callers that need
373
394
  // to *wait* in line use the realtime (WS-backed) `ablo.<model>.claim`.
374
395
  if (body.status === 'queued') {
375
- throw new AbloClaimedError(`Target ${intentOptions.target.model}/${intentOptions.target.id} is held; ` +
396
+ throw new AbloClaimedError(`Target ${claimOptions.target.model}/${claimOptions.target.id} is held; ` +
376
397
  `queued at position ${body.position ?? 0}. The HTTP client can't await ` +
377
- `the grant (no socket) — use the realtime client to wait in line.`, { code: 'intent_queued' });
398
+ `the grant (no socket) — use the realtime client to wait in line.`, { code: 'claim_queued' });
378
399
  }
379
- const id = body.intent?.id ?? intentId;
400
+ const id = body.claim?.id ?? claimId;
380
401
  let released = false;
381
402
  const release = async () => {
382
403
  if (released)
383
404
  return;
384
405
  released = true;
385
- await requestJson(`/v1/intents/${encodeURIComponent(id)}`, { method: 'DELETE' });
406
+ await requestJson(`/v1/claims/${encodeURIComponent(id)}`, { method: 'DELETE' });
386
407
  };
387
408
  return {
388
- id,
409
+ object: 'claim',
410
+ claimId: id,
411
+ action: claimOptions.action,
412
+ target: claimOptions.target,
389
413
  release,
390
414
  revoke: () => {
391
415
  void release().catch(() => { });
@@ -393,9 +417,9 @@ export function createProtocolClient(options) {
393
417
  [Symbol.asyncDispose]: release,
394
418
  };
395
419
  },
396
- list: listIntents,
420
+ list: listClaims,
397
421
  waitFor(target, options) {
398
- return waitForNoIntents(target, options);
422
+ return waitForNoClaims(target, options);
399
423
  },
400
424
  };
401
425
  async function listModel(modelName, options) {
@@ -456,7 +480,7 @@ export function createProtocolClient(options) {
456
480
  readAt: options.readAt,
457
481
  onStale: options.onStale,
458
482
  wait: options.wait,
459
- intent: options.intent,
483
+ claim: options.claim,
460
484
  }, `${modelName} ${action}`);
461
485
  const clientTxId = createClientTxId(options?.idempotencyKey);
462
486
  const encModel = encodeURIComponent(modelName);
@@ -475,7 +499,7 @@ export function createProtocolClient(options) {
475
499
  const readAt = options?.readAt ?? claimHandle?.readAt;
476
500
  const requestBody = {
477
501
  idempotencyKey: clientTxId,
478
- intent: normalizeIntentId(options?.intent) ?? claimHandle?.claimId,
502
+ claim: normalizeClaimId(options?.claimRef) ?? claimHandle?.claimId,
479
503
  onStale: options?.onStale ?? (claimHandle?.readAt !== undefined ? 'reject' : undefined),
480
504
  readAt,
481
505
  };
@@ -528,9 +552,9 @@ export function createProtocolClient(options) {
528
552
  });
529
553
  if (body.status === 'queued') {
530
554
  throw new AbloClaimedError(`Target ${name}/${params.id} is held; queued at position ${body.position ?? 0}. ` +
531
- `The HTTP client cannot await the grant without a WebSocket.`, { code: 'intent_queued' });
555
+ `The HTTP client cannot await the grant without a WebSocket.`, { code: 'claim_queued' });
532
556
  }
533
- return body.intent?.id ?? body.id ?? body.intentId ?? createIntentId();
557
+ return body.claim?.id ?? body.id ?? body.claimId ?? createClaimId();
534
558
  };
535
559
  const releaseClaim = (params) => requestJson(claimPath(isClaimHandle(params) ? params.target.id : params.id), { method: 'DELETE' }).then(() => undefined);
536
560
  async function claimImpl(params) {
@@ -559,23 +583,23 @@ export function createProtocolClient(options) {
559
583
  [Symbol.asyncDispose]: release,
560
584
  };
561
585
  }
562
- const intentsForEntity = async (params) => requestJson(`/v1/intents?model=${encodeURIComponent(name)}&id=${encodeURIComponent(params.id)}${params.field ? `&field=${encodeURIComponent(params.field)}` : ''}`, { method: 'GET' });
586
+ const claimsForEntity = async (params) => requestJson(`/v1/claims?model=${encodeURIComponent(name)}&id=${encodeURIComponent(params.id)}${params.field ? `&field=${encodeURIComponent(params.field)}` : ''}`, { method: 'GET' });
563
587
  const claim = Object.assign(claimImpl, {
564
588
  release: releaseClaim,
565
589
  state: async (params) => {
566
- const res = await intentsForEntity(params);
567
- return res.intents?.[0] ?? null;
590
+ const res = await claimsForEntity(params);
591
+ return res.claims?.[0] ?? null;
568
592
  },
569
593
  queue: async (params) => {
570
- const res = await intentsForEntity(params);
594
+ const res = await claimsForEntity(params);
571
595
  return { object: 'list', data: res.queue ?? [] };
572
596
  },
573
597
  reorder: async (params) => {
574
598
  await requestJson(`${claimPath(params.id)}/reorder`, {
575
599
  method: 'POST',
576
- // The reorder route's payload is `{ heldBy, intentId }[]` — Intent's id
577
- // IS the intentId.
578
- body: JSON.stringify({ order: params.order.map((i) => ({ heldBy: i.heldBy, intentId: i.id })) }),
600
+ // The reorder route's payload is `{ heldBy, claimId }[]` — Claim's id
601
+ // IS the claimId.
602
+ body: JSON.stringify({ order: params.order.map((i) => ({ heldBy: i.heldBy, claimId: i.id })) }),
579
603
  });
580
604
  },
581
605
  });
@@ -584,11 +608,11 @@ export function createProtocolClient(options) {
584
608
  if (!claimInput)
585
609
  return run(input);
586
610
  if (isClaimHandle(claimInput)) {
587
- return run({ ...input, intent: { id: claimInput.claimId }, claim: undefined });
611
+ return run({ ...input, claimRef: { id: claimInput.claimId }, claim: undefined });
588
612
  }
589
613
  const claimId = await acquireClaim({ id, ...claimInput });
590
614
  try {
591
- return await run({ ...input, intent: { id: claimId }, claim: undefined });
615
+ return await run({ ...input, claimRef: { id: claimId }, claim: undefined });
592
616
  }
593
617
  finally {
594
618
  await releaseClaim({ id }).catch(() => { });
@@ -624,12 +648,12 @@ export function createProtocolClient(options) {
624
648
  };
625
649
  }
626
650
  return {
627
- async ready() { },
651
+ ready,
628
652
  async waitForFlush() { },
629
653
  async dispose() { },
630
654
  async purge() { },
631
655
  capabilities,
632
- intents,
656
+ claims,
633
657
  commits,
634
658
  model,
635
659
  async getAuthToken() {
@@ -639,10 +663,10 @@ export function createProtocolClient(options) {
639
663
  },
640
664
  };
641
665
  }
642
- function normalizeIntentId(intent) {
643
- if (typeof intent === 'string')
644
- return intent;
645
- return intent?.id;
666
+ function normalizeClaimId(claim) {
667
+ if (typeof claim === 'string')
668
+ return claim;
669
+ return claim?.id;
646
670
  }
647
671
  function parseBody(bodyText) {
648
672
  if (bodyText.length === 0)
@@ -45,12 +45,17 @@ export declare function resolveAuthToken(input: AuthResolveInput): string | null
45
45
  /**
46
46
  * Resolve the direct-URL connector's Postgres connection string.
47
47
  *
48
- * The default Data Source path should not call this: the customer keeps
49
- * `DATABASE_URL` in their app and exposes `dataSource(...)`. This helper exists
50
- * only for the opt-in direct connector where Ablo registers a dedicated tenant
51
- * database. Returns null for Ablo-managed storage.
48
+ * `databaseUrl` is an EXPLICIT, opt-in option: Ablo registers a dedicated
49
+ * tenant database only when the caller passes it to `Ablo(...)`. It is NOT
50
+ * read from `process.env.DATABASE_URL` per this module's invariant
51
+ * (`ABLO_API_KEY` is the only environment fallback), an app's `DATABASE_URL`
52
+ * (commonly set for Prisma/Drizzle/docker) must never silently flip the client
53
+ * into connection-string mode. The default Data Source path keeps `DATABASE_URL`
54
+ * in the app and exposes `dataSource(...)`; that path leaves this null.
55
+ * `warnIfDatabaseUrlEnvIgnored` nudges callers who set the env but omitted the option.
52
56
  */
53
57
  export declare function resolveDatabaseUrl(input: AuthResolveInput): string | null;
58
+ export declare function warnIfDatabaseUrlEnvIgnored(input: AuthResolveInput, warn?: (message: string) => void): void;
54
59
  export declare const ABLO_HOSTED_API_DOMAIN = "api.abloatai.com";
55
60
  export declare const ABLO_HOSTED_HTTP_BASE_URL = "https://api.abloatai.com";
56
61
  export declare const ABLO_DEFAULT_BASE_URL = "https://api.abloatai.com";
@@ -30,13 +30,48 @@ export function resolveAuthToken(input) {
30
30
  /**
31
31
  * Resolve the direct-URL connector's Postgres connection string.
32
32
  *
33
- * The default Data Source path should not call this: the customer keeps
34
- * `DATABASE_URL` in their app and exposes `dataSource(...)`. This helper exists
35
- * only for the opt-in direct connector where Ablo registers a dedicated tenant
36
- * database. Returns null for Ablo-managed storage.
33
+ * `databaseUrl` is an EXPLICIT, opt-in option: Ablo registers a dedicated
34
+ * tenant database only when the caller passes it to `Ablo(...)`. It is NOT
35
+ * read from `process.env.DATABASE_URL` per this module's invariant
36
+ * (`ABLO_API_KEY` is the only environment fallback), an app's `DATABASE_URL`
37
+ * (commonly set for Prisma/Drizzle/docker) must never silently flip the client
38
+ * into connection-string mode. The default Data Source path keeps `DATABASE_URL`
39
+ * in the app and exposes `dataSource(...)`; that path leaves this null.
40
+ * `warnIfDatabaseUrlEnvIgnored` nudges callers who set the env but omitted the option.
37
41
  */
38
42
  export function resolveDatabaseUrl(input) {
39
- return input.options.databaseUrl ?? input.env.DATABASE_URL ?? null;
43
+ return input.options.databaseUrl ?? null;
44
+ }
45
+ /**
46
+ * One-time migration nudge for the dropped `DATABASE_URL` env fallback.
47
+ *
48
+ * Earlier versions silently adopted `process.env.DATABASE_URL` when `databaseUrl`
49
+ * was not passed, registering a direct connector behind the caller's back — which
50
+ * surprised any app that keeps `DATABASE_URL` for another tool (Prisma, Drizzle,
51
+ * docker-compose) and, on localhost, tried to register a database Ablo's cloud
52
+ * cannot reach. The env value is now ignored; this points the developer at the
53
+ * explicit option instead of flipping their mode for them. Warns once per process
54
+ * so it never spams, and falls back to `console.warn` when no logger is supplied
55
+ * (the `transport: 'api'` client has none).
56
+ */
57
+ let warnedDatabaseUrlEnvIgnored = false;
58
+ export function warnIfDatabaseUrlEnvIgnored(input, warn) {
59
+ if (warnedDatabaseUrlEnvIgnored)
60
+ return;
61
+ if (input.options.databaseUrl != null)
62
+ return;
63
+ const envUrl = input.env.DATABASE_URL;
64
+ if (typeof envUrl !== 'string' || envUrl.length === 0)
65
+ return;
66
+ warnedDatabaseUrlEnvIgnored = true;
67
+ const message = 'Found DATABASE_URL in the environment but `databaseUrl` was not passed to Ablo(...). ' +
68
+ 'Ablo no longer auto-adopts DATABASE_URL — the environment value is ignored. ' +
69
+ 'To register your Postgres directly, pass `databaseUrl: process.env.DATABASE_URL` explicitly; ' +
70
+ 'otherwise ignore this (the hosted sandbox and signed Data Source endpoints need no databaseUrl).';
71
+ if (warn)
72
+ warn(message);
73
+ else if (typeof console !== 'undefined')
74
+ console.warn('[sync]', message);
40
75
  }
41
76
  export const ABLO_HOSTED_API_DOMAIN = 'api.abloatai.com';
42
77
  export const ABLO_HOSTED_HTTP_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
@@ -21,7 +21,7 @@ import type { SyncClient } from '../SyncClient.js';
21
21
  import type { HydrationCoordinator } from '../sync/HydrationCoordinator.js';
22
22
  import type { LoadWhere } from '../query/types.js';
23
23
  import { ModelScope } from '../types/index.js';
24
- import type { Duration, Intent, IntentWaitOptions, Snapshot, TargetRange } from '../types/streams.js';
24
+ import type { Duration, Claim, ClaimHandle, ClaimWaitOptions, Snapshot, TargetRange } from '../types/streams.js';
25
25
  export interface ModelClientMeta {
26
26
  readonly key: string;
27
27
  readonly typename: string;
@@ -73,21 +73,8 @@ export interface ModelLoadOptions<T> {
73
73
  /** Options for the single-row async server read `retrieve({ id })`. A subset of
74
74
  * {@link ModelLoadOptions} — `where`/`limit`/`orderBy` are fixed by the id. */
75
75
  export type ModelRetrieveOptions = Pick<ModelLoadOptions<unknown>, 'type' | 'expand'>;
76
- export interface IntentLeaseHandle {
77
- readonly id: string;
78
- /**
79
- * True when the grant came AFTER waiting in the server's FIFO line
80
- * (`intent_granted`) — the authoritative "the row may have changed
81
- * underneath us" signal. The local `observe()` snapshot can't stand in
82
- * for this: intent fan-out is entity-scoped, so org-wide subscriptions
83
- * (the default hosted client) never see peers' claims at all.
84
- */
85
- readonly waited?: boolean;
86
- release(): Promise<void>;
87
- revoke(): void;
88
- }
89
76
  export interface ModelCollaboration<T> {
90
- createIntent(options: {
77
+ createClaim(options: {
91
78
  target: {
92
79
  model: string;
93
80
  id: string;
@@ -106,11 +93,11 @@ export interface ModelCollaboration<T> {
106
93
  queue?: boolean;
107
94
  /** Reject (don't wait) if the queue is already this deep when we join. */
108
95
  maxQueueDepth?: number;
109
- }): Promise<IntentLeaseHandle>;
96
+ }): Promise<ClaimHandle>;
110
97
  createSnapshot(modelKey: string, id: string): Snapshot;
111
98
  /**
112
99
  * Current coordination state on a target — who (if anyone) holds it.
113
- * Synchronous reactive snapshot read off the presence/intent stream;
100
+ * Synchronous reactive snapshot read off the presence/claim stream;
114
101
  * `null` when the target is free. The wiring site computes it because
115
102
  * only it knows the local participant id (needed to distinguish "I
116
103
  * hold it" from "someone else holds it").
@@ -118,15 +105,15 @@ export interface ModelCollaboration<T> {
118
105
  observe(target: {
119
106
  model: string;
120
107
  id: string;
121
- }): Intent | null;
108
+ }): Claim | null;
122
109
  /**
123
- * The reactive wait queue on a target — the FIFO line of queued intents
124
- * behind the holder. Synchronous snapshot off the synced intent stream.
110
+ * The reactive wait queue on a target — the FIFO line of queued claims
111
+ * behind the holder. Synchronous snapshot off the synced claim stream.
125
112
  */
126
113
  queue(target: {
127
114
  model: string;
128
115
  id: string;
129
- }): readonly Intent[];
116
+ }): readonly Claim[];
130
117
  /**
131
118
  * Re-rank the wait queue on a target (privileged — server-gated). `order` is
132
119
  * the desired front-of-line ordering, taken from `queue(target)`.
@@ -134,21 +121,46 @@ export interface ModelCollaboration<T> {
134
121
  reorder(target: {
135
122
  model: string;
136
123
  id: string;
137
- }, order: readonly Intent[]): void;
124
+ }, order: readonly Claim[]): void;
138
125
  /**
139
- * Resolve once no participant holds an active intent on the target.
140
- * The contender's "wait until it's free" — delegates to the intent
126
+ * Resolve once no participant holds an active claim on the target.
127
+ * The contender's "wait until it's free" — delegates to the claim
141
128
  * stream's `waitFor`.
142
129
  */
143
130
  waitFor(target: {
144
131
  model: string;
145
132
  id: string;
146
- }, options?: IntentWaitOptions): Promise<void>;
133
+ }, options?: ClaimWaitOptions): Promise<void>;
147
134
  /**
148
135
  * The local participant's id. Used to distinguish "I already hold this"
149
136
  * from "someone else holds it" in `claimOrWait`.
150
137
  */
151
138
  readonly selfParticipantId: string;
139
+ /**
140
+ * The local participant's kind (`'user' | 'agent' | 'system'`). Used to
141
+ * stamp the synthesized self-claim returned from `claim.state` when the
142
+ * LOCAL proxy holds the lease (server presence frames exclude one's own
143
+ * claims, so the holder must build its own view).
144
+ */
145
+ readonly selfParticipantKind?: 'user' | 'agent' | 'system';
146
+ /**
147
+ * Subscribe the connection to a scope's sync group(s) (read-interest).
148
+ * The typed surface calls this on single-entity reads/claim observation so
149
+ * a Node/agent client lands in the SAME entity-scoped group the holder's
150
+ * claim presence fans out on — otherwise a peer subscribed only to
151
+ * `org:`/`user:` groups never sees claim broadcasts. Fire-and-forget and
152
+ * SOFT: read interest is best-effort and must never make a read reject or
153
+ * stall (see `AreaOfInterestManager.reconcile`). Optional so minimal test
154
+ * doubles can omit it. Forwards to `BaseSyncedStore.enterScope`.
155
+ */
156
+ enterScope?(scope: Record<string, string>): void | Promise<void>;
157
+ /**
158
+ * Pin a scope's sync group(s) (write-intent / prominence): a row this
159
+ * client holds an active claim on stays subscribed regardless of
160
+ * navigation. Same fire-and-forget, soft semantics as `enterScope`.
161
+ * Forwards to `BaseSyncedStore.pinScope`.
162
+ */
163
+ pinScope?(scope: Record<string, string>): void | Promise<void>;
152
164
  }
153
165
  export interface ClaimTargetOptions<T = Record<string, unknown>> {
154
166
  /** Phase shown to observers while held. Defaults to `'editing'`. */
@@ -191,7 +203,7 @@ export interface ClaimLookupParams<T = Record<string, unknown>> {
191
203
  readonly field?: string;
192
204
  }
193
205
  export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLookupParams<T> {
194
- readonly order: readonly Intent[];
206
+ readonly order: readonly Claim[];
195
207
  }
196
208
  /**
197
209
  * A claim handle: the held entity data plus an explicit release hook, so
@@ -217,32 +229,7 @@ export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLo
217
229
  * `ablo.<model>.update({ id, data, claim })` verb — the handle carries the
218
230
  * lease id and snapshot watermark for attribution + stale protection.
219
231
  */
220
- export interface ClaimHandle<T = Record<string, unknown>> extends AsyncDisposable {
221
- readonly object: 'claim';
222
- readonly claimId: string;
223
- /**
224
- * Sync watermark of the held snapshot (`data` was read at this stamp).
225
- * Writes that carry the handle — `update({ id, data, claim })` or
226
- * `commits.create({ claim, ... })` — use it as the `readAt` stale guard,
227
- * so a concurrent commit between snapshot and write is rejected instead
228
- * of clobbered. Optional for wire/duck-type compat with externally
229
- * constructed handles.
230
- */
231
- readonly readAt?: number;
232
- readonly target: {
233
- readonly model: string;
234
- readonly id: string;
235
- readonly field?: string;
236
- readonly path?: string;
237
- readonly range?: TargetRange;
238
- readonly meta?: Record<string, unknown>;
239
- };
240
- readonly action: string;
241
- readonly description?: string;
242
- readonly data: T;
243
- release(): Promise<void>;
244
- revoke(): void;
245
- }
232
+ export type { ClaimHandle };
246
233
  export type ClaimOptions<T = Record<string, unknown>> = ClaimTargetOptions<T>;
247
234
  /**
248
235
  * The coordination surface for a model, exposed as a callable namespace.
@@ -273,14 +260,14 @@ export interface ClaimApi<T> {
273
260
  * Current holder for a row, or `null` when free. Use this for UI badges and
274
261
  * preflight checks, not for the normal write path.
275
262
  */
276
- state(params: ClaimLookupParams<T>): Intent | null;
263
+ state(params: ClaimLookupParams<T>): Claim | null;
277
264
  /**
278
265
  * FIFO wait line behind the current holder. Advanced: useful for operator
279
266
  * UIs and schedulers.
280
267
  */
281
268
  queue(params: ClaimLookupParams<T>): {
282
269
  readonly object: 'list';
283
- readonly data: readonly Intent[];
270
+ readonly data: readonly Claim[];
284
271
  };
285
272
  /**
286
273
  * Re-rank the wait line. Advanced and permission-gated.