@abloatai/ablo 0.29.2 → 0.30.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 (118) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +3 -2
  3. package/dist/BaseSyncedStore.js +3 -1
  4. package/dist/Database.js +29 -2
  5. package/dist/SyncClient.d.ts +5 -3
  6. package/dist/SyncClient.js +8 -4
  7. package/dist/ai-sdk/coordinatedTool.d.ts +0 -1
  8. package/dist/ai-sdk/coordinatedTool.js +0 -1
  9. package/dist/ai-sdk/coordinationContext.js +1 -5
  10. package/dist/cli.cjs +561 -90
  11. package/dist/client/Ablo.d.ts +1 -1
  12. package/dist/client/Ablo.js +24 -36
  13. package/dist/client/auth.d.ts +8 -18
  14. package/dist/client/auth.js +16 -96
  15. package/dist/client/createModelProxy.d.ts +16 -11
  16. package/dist/client/createModelProxy.js +162 -20
  17. package/dist/client/httpClient.d.ts +1 -1
  18. package/dist/client/httpTransport.js +263 -89
  19. package/dist/client/options.d.ts +0 -15
  20. package/dist/client/resourceTypes.d.ts +13 -26
  21. package/dist/client/schemaConfig.js +5 -0
  22. package/dist/client/writeOptionsSchema.d.ts +1 -0
  23. package/dist/client/writeOptionsSchema.js +11 -2
  24. package/dist/client/wsMutationExecutor.d.ts +3 -5
  25. package/dist/coordination/schema.d.ts +17 -13
  26. package/dist/coordination/schema.js +36 -10
  27. package/dist/errorCodes.d.ts +9 -4
  28. package/dist/errorCodes.js +10 -5
  29. package/dist/errors.js +4 -9
  30. package/dist/index.d.ts +4 -0
  31. package/dist/interfaces/index.d.ts +31 -19
  32. package/dist/schema/index.d.ts +1 -1
  33. package/dist/schema/index.js +4 -1
  34. package/dist/schema/schema.d.ts +13 -0
  35. package/dist/schema/select.js +7 -0
  36. package/dist/schema/syncDeltaRow.d.ts +31 -13
  37. package/dist/schema/syncDeltaRow.js +33 -15
  38. package/dist/schema/tenancy.d.ts +52 -16
  39. package/dist/schema/tenancy.js +86 -25
  40. package/dist/server/commit.d.ts +8 -24
  41. package/dist/server/index.d.ts +1 -0
  42. package/dist/server/index.js +1 -0
  43. package/dist/server/readConfig.d.ts +10 -0
  44. package/dist/source/adapter.d.ts +26 -15
  45. package/dist/source/adapter.js +5 -3
  46. package/dist/source/adapters/drizzle.js +35 -9
  47. package/dist/source/adapters/kysely.d.ts +33 -61
  48. package/dist/source/adapters/kysely.js +170 -177
  49. package/dist/source/adapters/kyselyMutationCore.d.ts +76 -0
  50. package/dist/source/adapters/kyselyMutationCore.js +125 -0
  51. package/dist/source/adapters/memory.d.ts +1 -1
  52. package/dist/source/adapters/memory.js +24 -10
  53. package/dist/source/adapters/prisma.js +35 -8
  54. package/dist/source/conformance.d.ts +26 -28
  55. package/dist/source/conformance.js +142 -69
  56. package/dist/source/contract.d.ts +46 -4
  57. package/dist/source/contract.js +66 -5
  58. package/dist/source/factory.d.ts +3 -3
  59. package/dist/source/factory.js +24 -3
  60. package/dist/source/idempotency.d.ts +46 -0
  61. package/dist/source/idempotency.js +129 -0
  62. package/dist/source/index.d.ts +6 -4
  63. package/dist/source/index.js +5 -3
  64. package/dist/source/migrations.d.ts +9 -5
  65. package/dist/source/migrations.js +39 -6
  66. package/dist/source/types.d.ts +88 -14
  67. package/dist/source/types.js +16 -0
  68. package/dist/surface.d.ts +1 -1
  69. package/dist/surface.js +0 -1
  70. package/dist/sync/BootstrapFetcher.js +7 -0
  71. package/dist/sync/SyncWebSocket.d.ts +5 -27
  72. package/dist/sync/awaitClaimGrant.d.ts +6 -0
  73. package/dist/sync/awaitClaimGrant.js +12 -2
  74. package/dist/sync/commitFrames.d.ts +2 -4
  75. package/dist/sync/commitFrames.js +1 -0
  76. package/dist/sync/createClaimStream.d.ts +1 -1
  77. package/dist/sync/createClaimStream.js +13 -15
  78. package/dist/sync/deltaPipeline.d.ts +1 -1
  79. package/dist/sync/deltaPipeline.js +1 -1
  80. package/dist/sync/participants.d.ts +2 -3
  81. package/dist/sync/participants.js +2 -2
  82. package/dist/sync/wsFrameHandlers.js +35 -15
  83. package/dist/testing/mocks/MockMutationExecutor.d.ts +21 -0
  84. package/dist/testing/mocks/MockMutationExecutor.js +53 -1
  85. package/dist/transactions/TransactionQueue.d.ts +61 -9
  86. package/dist/transactions/TransactionQueue.js +502 -114
  87. package/dist/transactions/commitEnvelope.d.ts +4 -0
  88. package/dist/transactions/commitEnvelope.js +25 -0
  89. package/dist/transactions/commitPayload.d.ts +23 -5
  90. package/dist/transactions/commitPayload.js +3 -0
  91. package/dist/transactions/deltaConfirmation.d.ts +2 -1
  92. package/dist/transactions/deltaConfirmation.js +22 -4
  93. package/dist/transactions/durableWriteStore.d.ts +8 -1
  94. package/dist/transactions/httpCommitEnvelope.d.ts +3 -1
  95. package/dist/transactions/httpCommitEnvelope.js +30 -0
  96. package/dist/transactions/replayValidation.d.ts +4 -0
  97. package/dist/transactions/replayValidation.js +6 -0
  98. package/dist/types/streams.d.ts +29 -17
  99. package/dist/wire/commit.d.ts +671 -0
  100. package/dist/wire/commit.js +300 -0
  101. package/dist/wire/delta.d.ts +4 -0
  102. package/dist/wire/delta.js +9 -0
  103. package/dist/wire/frames.d.ts +17 -45
  104. package/dist/wire/index.d.ts +2 -0
  105. package/dist/wire/index.js +4 -0
  106. package/docs/agent-messaging.md +4 -5
  107. package/docs/agents.md +1 -2
  108. package/docs/api-keys.md +1 -2
  109. package/docs/client-behavior.md +4 -6
  110. package/docs/coordination.md +64 -9
  111. package/docs/data-sources.md +4 -7
  112. package/docs/integration-guide.md +6 -8
  113. package/docs/migration.md +0 -3
  114. package/docs/quickstart.md +7 -8
  115. package/docs/schema-contract.md +7 -8
  116. package/package.json +4 -4
  117. package/dist/client/registerDataSource.d.ts +0 -19
  118. package/dist/client/registerDataSource.js +0 -66
@@ -30,7 +30,7 @@ import { type AbloHttpClient, type AbloHttpClientOptions } from './httpClient.js
30
30
  import type { AbloOptions } from './options.js';
31
31
  import type { AbloSession, CommitResource, CreateAgentClientParams, CreateSessionParams } from './resourceTypes.js';
32
32
  export type { ApiKeySetter, AbloOptions, InternalAbloOptions } from './options.js';
33
- export type { LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, ModelOperations, ModelOperationAction, CommitWait, IfClaimedPolicy, ClaimedOptions, ClaimWaitOptions, ModelReadOptions, ClaimCreateOptions, CommitOperationInput, CommitCreateOptions, CommitReceipt, CommitResource, ClaimResource, ModelMutationOptions, HttpClaimApi, SessionOperation, CreateUserSessionParams, CreateAgentSessionParams, CreateSessionParams, CreateAgentClientParams, AbloSession, } from './resourceTypes.js';
33
+ export type { LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, HeldLease, ModelOperations, ModelOperationAction, CommitWait, IfClaimedPolicy, ClaimedOptions, ClaimWaitOptions, ModelReadOptions, ClaimCreateOptions, CommitOperationInput, CommitCreateOptions, CommitReceipt, CommitResource, ClaimResource, ModelMutationOptions, HttpClaimApi, SessionOperation, CreateUserSessionParams, CreateAgentSessionParams, CreateSessionParams, CreateAgentClientParams, AbloSession, } from './resourceTypes.js';
34
34
  export { computeFKDepthPriority } from './schemaConfig.js';
35
35
  import type { ModelOperations } from './createModelProxy.js';
36
36
  /**
@@ -20,7 +20,6 @@
20
20
  */
21
21
  import { durableCommitOperationSchema, } from '../transactions/commitEnvelope.js';
22
22
  import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, toAbloError, claimedError } from '../errors.js';
23
- import { descriptionFromMeta } from '../coordination/schema.js';
24
23
  import { initSyncEngine } from '../context.js';
25
24
  import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../SyncEngineContext.js';
26
25
  import { alwaysOnline } from '../adapters/alwaysOnline.js';
@@ -40,8 +39,7 @@ import { createParticipantManager } from '../sync/participants.js';
40
39
  // Value import is cycle-safe: httpClient.js and httpTransport.js take the client
41
40
  // types from the `options`/`resourceTypes` leaves, never from this module.
42
41
  import { createAbloHttpClient, } from './httpClient.js';
43
- import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfCliKeyMismatch, warnIfDatabaseUrlEnvIgnored, warnIfDatabaseUrlDeprecated, } from './auth.js';
44
- import { registerDataSource } from './registerDataSource.js';
42
+ import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, rejectRemovedDatabaseUrlOption, warnIfCliKeyMismatch, } from './auth.js';
45
43
  import { shouldUseInMemoryPersistence } from './persistence.js';
46
44
  import { deriveConfigFromSchema } from './schemaConfig.js';
47
45
  import { registerModelsFromSchema } from './modelRegistration.js';
@@ -89,20 +87,15 @@ export function Ablo(options) {
89
87
  const authCredentials = createAuthCredentialSource(
90
88
  // eslint-disable-next-line @typescript-eslint/no-deprecated -- load-bearing on the self-hosted path; server-internal cap-mint (Phase 3) not shipped
91
89
  internalOptions.capabilityToken ?? configuredAuthToken);
92
- const configuredDatabaseUrl = resolveDatabaseUrl(authInput);
90
+ rejectRemovedDatabaseUrlOption(options);
93
91
  assertBrowserSafety({
94
92
  apiKey: configuredApiKey,
95
- databaseUrl: configuredDatabaseUrl,
96
93
  dangerouslyAllowBrowser: options.dangerouslyAllowBrowser,
97
94
  });
98
95
  // Custom logger wins; otherwise build the default `[Ablo]` logger at the level
99
96
  // resolved from `debug`/`logLevel`/`ABLO_LOG_LEVEL` (default `warn`).
100
97
  const logger = internalOptions.logger ??
101
98
  createConsoleLogger(resolveLogLevel({ debug: options.debug, logLevel: options.logLevel }));
102
- // Nudge (once) if a stray DATABASE_URL is in the env but `databaseUrl` wasn't
103
- // passed — the env value is no longer auto-adopted (see resolveDatabaseUrl).
104
- warnIfDatabaseUrlEnvIgnored(authInput, (m) => { logger.warn(m); });
105
- warnIfDatabaseUrlDeprecated(authInput, (m) => { logger.warn(m); });
106
99
  void warnIfCliKeyMismatch(authInput, (m) => { logger.warn(m); });
107
100
  const schema = options.schema;
108
101
  const url = resolveBaseURL(authInput);
@@ -317,19 +310,6 @@ export function Ablo(options) {
317
310
  }
318
311
  authCredentials.setAuthToken(token);
319
312
  }
320
- // Register the caller's own database for write-back before bootstrap, so
321
- // the server resolves this org's data plane to the customer's DB rather
322
- // than serving an empty/wrong store. The org is derived server-side from
323
- // the API key. Idempotent server-side (register-or-update). Skipped when
324
- // no `databaseUrl` was configured (Ablo-managed storage).
325
- if (configuredDatabaseUrl) {
326
- await registerDataSource({
327
- baseUrl: resolveBootstrapBaseUrl({ url }),
328
- apiKey: await resolveApiKeyValue(configuredApiKey),
329
- databaseUrl: configuredDatabaseUrl,
330
- ...(internalOptions.fetch ? { fetchImpl: internalOptions.fetch } : {}),
331
- });
332
- }
333
313
  // Resolve participant identity + scope. Three branches —
334
314
  // hosted-cloud apiKey exchange, self-derived from capability
335
315
  // token, or legacy explicit options. See `./identity.ts`.
@@ -490,7 +470,7 @@ export function Ablo(options) {
490
470
  ? crypto.randomUUID()
491
471
  : `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
492
472
  }
493
- function normalizeCommitOperation(op, defaults) {
473
+ function normalizeCommitOperation(op, defaults, fenceToken) {
494
474
  const type = op.action.toUpperCase();
495
475
  const id = op.id ?? '';
496
476
  return durableCommitOperationSchema.parse({
@@ -501,22 +481,23 @@ export function Ablo(options) {
501
481
  transactionId: op.transactionId ?? undefined,
502
482
  readAt: op.readAt ?? defaults.readAt ?? undefined,
503
483
  onStale: op.onStale ?? defaults.onStale ?? undefined,
484
+ // The batch's claim (if any) supplies one token for every op, mirroring
485
+ // how it supplies the batch `readAt`.
486
+ fenceToken: op.fenceToken ?? fenceToken ?? undefined,
504
487
  });
505
488
  }
506
- function normalizeCommitOperations(commitOptions) {
489
+ function normalizeCommitOperations(commitOptions, fenceToken) {
507
490
  if (commitOptions.operations.length === 0) {
508
491
  throw new AbloValidationError('Commit requires a non-empty `operations` array.', { code: 'commit_operation_required' });
509
492
  }
510
- return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions));
493
+ return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions, fenceToken));
511
494
  }
512
495
  function modelClaimFromActive(claim) {
513
- const description = descriptionFromMeta(claim.target.meta);
514
496
  return {
515
497
  id: claim.id,
516
498
  actor: claim.heldBy ?? "",
517
499
  participantKind: claim.participantKind ?? "user",
518
- reason: claim.reason,
519
- ...(description ? { description } : {}),
500
+ description: claim.description,
520
501
  field: claim.target.field,
521
502
  status: 'active',
522
503
  expiresAt: claim.expiresAt ?? 0,
@@ -595,16 +576,21 @@ export function Ablo(options) {
595
576
  }
596
577
  });
597
578
  }
598
- function wrapClaimHandle(claim, waited = false) {
579
+ function wrapClaimHandle(claim, waited = false, fenceToken) {
599
580
  const release = async () => {
600
581
  claim.revoke?.();
601
582
  };
583
+ // The token is server-stamped and arrives on the grant frame, so prefer
584
+ // the one `awaitClaimGrant` read there; fall back to any the local handle
585
+ // already carried (immediate, non-queued grants).
586
+ const resolvedFenceToken = fenceToken ?? claim.fenceToken;
602
587
  return {
603
588
  object: 'claim',
604
589
  id: claim.id,
605
- reason: claim.reason,
590
+ description: claim.description,
606
591
  target: claim.target,
607
592
  waited,
593
+ ...(resolvedFenceToken !== undefined ? { fenceToken: resolvedFenceToken } : {}),
608
594
  release,
609
595
  revoke: claim.revoke,
610
596
  // The lease-control members are forwarded explicitly — this wrapper
@@ -625,7 +611,7 @@ export function Ablo(options) {
625
611
  field: claimOptions.target.field,
626
612
  meta: claimOptions.target.meta,
627
613
  }, {
628
- reason: claimOptions.reason,
614
+ description: claimOptions.description,
629
615
  ttl: claimOptions.ttl,
630
616
  queue: claimOptions.queue,
631
617
  });
@@ -635,11 +621,12 @@ export function Ablo(options) {
635
621
  // callers — chiefly `ablo.<model>.claim` — get a handle that already
636
622
  // holds the lease, never a half-claimed one racing the queue.
637
623
  let waited = false;
624
+ let fenceToken;
638
625
  if (claimOptions.queue) {
639
626
  const ws = store.getSyncWebSocket();
640
627
  if (ws) {
641
628
  try {
642
- ({ waited } = await awaitClaimGrant(ws, claim.id, {
629
+ ({ waited, fenceToken } = await awaitClaimGrant(ws, claim.id, {
643
630
  timeoutMs: claimOptions.waitTimeoutMs,
644
631
  maxQueueDepth: claimOptions.maxQueueDepth,
645
632
  }));
@@ -653,7 +640,7 @@ export function Ablo(options) {
653
640
  }
654
641
  }
655
642
  }
656
- return wrapClaimHandle(claim, waited);
643
+ return wrapClaimHandle(claim, waited, fenceToken);
657
644
  },
658
645
  list(target) {
659
646
  return listModelClaims(target);
@@ -708,7 +695,7 @@ export function Ablo(options) {
708
695
  ...(held.target.field ? { field: held.target.field } : {}),
709
696
  ...(held.target.meta ? { meta: held.target.meta } : {}),
710
697
  },
711
- reason: held.reason,
698
+ description: held.description ?? 'editing',
712
699
  heldBy: held.actor,
713
700
  participantKind: held.participantKind,
714
701
  expiresAt: held.expiresAt,
@@ -757,7 +744,7 @@ export function Ablo(options) {
757
744
  ...commitOptions,
758
745
  readAt: commitOptions.readAt ?? claim?.readAt ?? null,
759
746
  onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
760
- });
747
+ }, claim?.fenceToken ?? null);
761
748
  const wait = commitOptions.wait ?? 'confirmed';
762
749
  // Route through the TransactionQueue's commit lane so the call
763
750
  // tolerates WS disconnects: the envelope stays in memory until
@@ -775,12 +762,13 @@ export function Ablo(options) {
775
762
  if (wait === 'queued') {
776
763
  return { id: clientTxId, status: 'queued' };
777
764
  }
778
- const { lastSyncId, notifications } = await queue.waitForCommitReceipt(clientTxId);
765
+ const { lastSyncId, notifications, missingIds } = await queue.waitForCommitReceipt(clientTxId);
779
766
  return {
780
767
  id: clientTxId,
781
768
  status: 'confirmed',
782
769
  lastSyncId,
783
770
  ...(notifications && notifications.length > 0 ? { notifications } : {}),
771
+ ...(missingIds && missingIds.length > 0 ? { missingIds } : {}),
784
772
  };
785
773
  },
786
774
  };
@@ -30,7 +30,6 @@ export interface AuthResolveInput {
30
30
  readonly authEndpoint?: string | ApiKeySetter | null;
31
31
  readonly authToken?: string | null;
32
32
  readonly baseURL?: string | null;
33
- readonly databaseUrl?: string | null;
34
33
  readonly dangerouslyAllowBrowser?: boolean;
35
34
  };
36
35
  readonly env: Record<string, string | undefined>;
@@ -66,22 +65,6 @@ export interface CliKeyMismatch {
66
65
  /** Infer the sandbox or production mode from an Ablo key's prefix. */
67
66
  export declare function modeFromApiKey(key: string): CliMode | undefined;
68
67
  export declare function describeCliKeyMismatch(configured: StaticApiKey, cli: CliCredentialSnapshot): CliKeyMismatch | null;
69
- /**
70
- * Resolves the Postgres connection string for the direct-connection option, or
71
- * `null` when none was given.
72
- *
73
- * `databaseUrl` is opt-in: the client registers a dedicated database only when
74
- * the caller passes it explicitly. It is never read from
75
- * `process.env.DATABASE_URL`, because this module treats `ABLO_API_KEY` as the
76
- * one environment fallback — an app's `DATABASE_URL`, commonly set for other
77
- * tools, must not silently switch the client into connection-string mode. The
78
- * default path leaves `DATABASE_URL` untouched and reads through `dataSource(...)`
79
- * instead, so this returns `null`. {@link warnIfDatabaseUrlEnvIgnored} nudges a
80
- * caller who set the environment variable but omitted the option.
81
- */
82
- export declare function resolveDatabaseUrl(input: AuthResolveInput): string | null;
83
- export declare function warnIfDatabaseUrlEnvIgnored(input: AuthResolveInput, warn?: (message: string) => void): void;
84
- export declare function warnIfDatabaseUrlDeprecated(input: AuthResolveInput, warn?: (message: string) => void): void;
85
68
  export declare function warnIfCliKeyMismatch(input: AuthResolveInput, warn?: (message: string) => void): Promise<void>;
86
69
  export { ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, ABLO_DEFAULT_BASE_URL } from './hostedEndpoints.js';
87
70
  /**
@@ -99,9 +82,16 @@ export declare function resolveBaseURL(input: AuthResolveInput): string;
99
82
  * server proxy. Throws {@link AbloAuthenticationError} when a secret key is
100
83
  * detected in a browser without opt-in.
101
84
  */
85
+ /**
86
+ * Rejects the REMOVED `databaseUrl` dial-in option loudly. The option is gone
87
+ * from the types, but an untyped or stale caller could still pass it — and
88
+ * silently ignoring it would reroute their writes to Ablo-hosted storage,
89
+ * the opposite of what that option used to promise. A construction-time throw
90
+ * turns a wrong-storage surprise into a clear migration instruction.
91
+ */
92
+ export declare function rejectRemovedDatabaseUrlOption(options: object): void;
102
93
  export declare function assertBrowserSafety(input: {
103
94
  apiKey: string | ApiKeySetter | null;
104
- databaseUrl?: string | null;
105
95
  dangerouslyAllowBrowser: boolean | undefined;
106
96
  }): void;
107
97
  /**
@@ -223,94 +223,6 @@ export function describeCliKeyMismatch(configured, cli) {
223
223
  }
224
224
  return null;
225
225
  }
226
- /**
227
- * Resolves the Postgres connection string for the direct-connection option, or
228
- * `null` when none was given.
229
- *
230
- * `databaseUrl` is opt-in: the client registers a dedicated database only when
231
- * the caller passes it explicitly. It is never read from
232
- * `process.env.DATABASE_URL`, because this module treats `ABLO_API_KEY` as the
233
- * one environment fallback — an app's `DATABASE_URL`, commonly set for other
234
- * tools, must not silently switch the client into connection-string mode. The
235
- * default path leaves `DATABASE_URL` untouched and reads through `dataSource(...)`
236
- * instead, so this returns `null`. {@link warnIfDatabaseUrlEnvIgnored} nudges a
237
- * caller who set the environment variable but omitted the option.
238
- */
239
- export function resolveDatabaseUrl(input) {
240
- return input.options.databaseUrl ?? null;
241
- }
242
- /**
243
- * Warns once when `DATABASE_URL` is set in the environment but `databaseUrl` was
244
- * not passed as an option.
245
- *
246
- * The client does not adopt `process.env.DATABASE_URL` on its own, because that
247
- * value is commonly set for other tools and switching the client into
248
- * connection-string mode behind the caller's back is surprising — and on
249
- * localhost it would try to register a database the hosted service cannot reach.
250
- * This warning points the developer at the explicit option instead. It fires at
251
- * most once per process and falls back to `console.warn` when no logger is
252
- * supplied.
253
- *
254
- * The warning is skipped entirely when an `apiKey` resolves (from the option or
255
- * `ABLO_API_KEY`): that caller has chosen the hosted, token-based transport,
256
- * which is separate from the direct `databaseUrl` connection. A `DATABASE_URL`
257
- * present in that environment belongs to unrelated infrastructure, not an omitted
258
- * option, so warning would be a false positive.
259
- */
260
- let warnedDatabaseUrlEnvIgnored = false;
261
- export function warnIfDatabaseUrlEnvIgnored(input, warn) {
262
- if (warnedDatabaseUrlEnvIgnored)
263
- return;
264
- if (input.options.databaseUrl != null)
265
- return;
266
- // Hosted/token path → DATABASE_URL is unrelated infra, not an omitted option.
267
- if (resolveApiKey(input) != null)
268
- return;
269
- const envUrl = input.env.DATABASE_URL;
270
- if (typeof envUrl !== 'string' || envUrl.length === 0)
271
- return;
272
- warnedDatabaseUrlEnvIgnored = true;
273
- const message = 'Found DATABASE_URL in the environment but `databaseUrl` was not passed to Ablo(...). ' +
274
- 'Ablo no longer auto-adopts DATABASE_URL — the environment value is ignored. ' +
275
- 'To register your Postgres directly, pass `databaseUrl: process.env.DATABASE_URL` explicitly; ' +
276
- 'otherwise ignore this (the hosted sandbox and signed Data Source endpoints need no databaseUrl).';
277
- if (warn)
278
- warn(message);
279
- else if (typeof console !== 'undefined')
280
- console.warn('[Ablo]', message);
281
- }
282
- /**
283
- * Warns once when the deprecated `databaseUrl` option is used.
284
- *
285
- * Passing `databaseUrl` opens a connection pool directly into your Postgres and
286
- * writes to it. That option is deprecated. Ablo is designed to host only the
287
- * ordered transaction log (the `sync_deltas` table) and coordination state,
288
- * never your rows — your data stays in your own database. The supported path is
289
- * a signed data-source endpoint (`dataSource(...)`), where your app owns the
290
- * write and your database credentials never leave it.
291
- *
292
- * The option still works at runtime so existing integrations keep running. This
293
- * warning fires at most once per process and falls back to `console.warn` when
294
- * no logger is supplied.
295
- */
296
- let warnedDatabaseUrlDeprecated = false;
297
- export function warnIfDatabaseUrlDeprecated(input, warn) {
298
- if (warnedDatabaseUrlDeprecated)
299
- return;
300
- if (input.options.databaseUrl == null)
301
- return;
302
- warnedDatabaseUrlDeprecated = true;
303
- const message = '`databaseUrl` (the direct connector) is deprecated and will be removed from ' +
304
- 'the supported path. It lets Ablo dial into your database; we are moving off ' +
305
- 'that. Ablo hosts only the transaction log — your data stays in your DB. Expose ' +
306
- 'a signed Data Source endpoint (`dataSource(...)`) so your app owns the write, ' +
307
- 'or self-host the engine to keep the log in your infra too. ' +
308
- 'See docs/plans/stripe-shaped-storage-posture.md.';
309
- if (warn)
310
- warn(message);
311
- else if (typeof console !== 'undefined')
312
- console.warn('[Ablo]', message);
313
- }
314
226
  let warnedCliKeyMismatch = false;
315
227
  export async function warnIfCliKeyMismatch(input, warn) {
316
228
  if (warnedCliKeyMismatch)
@@ -392,6 +304,22 @@ export function resolveBaseURL(input) {
392
304
  * server proxy. Throws {@link AbloAuthenticationError} when a secret key is
393
305
  * detected in a browser without opt-in.
394
306
  */
307
+ /**
308
+ * Rejects the REMOVED `databaseUrl` dial-in option loudly. The option is gone
309
+ * from the types, but an untyped or stale caller could still pass it — and
310
+ * silently ignoring it would reroute their writes to Ablo-hosted storage,
311
+ * the opposite of what that option used to promise. A construction-time throw
312
+ * turns a wrong-storage surprise into a clear migration instruction.
313
+ */
314
+ export function rejectRemovedDatabaseUrlOption(options) {
315
+ const legacyValue = Reflect.get(options, 'databaseUrl');
316
+ if (legacyValue == null)
317
+ return;
318
+ throw new AbloValidationError('The `databaseUrl` option was removed. Your database connects through ' +
319
+ '`ablo connect` now: Ablo writes it directly with a scoped DML role and ' +
320
+ 'confirms through its replication stream. Run `npx ablo connect` to ' +
321
+ 'register it, then construct the client with your API key only.', { code: 'invalid_body' });
322
+ }
395
323
  export function assertBrowserSafety(input) {
396
324
  const inBrowser = typeof window !== 'undefined';
397
325
  if (!input.dangerouslyAllowBrowser &&
@@ -405,14 +333,6 @@ export function assertBrowserSafety(input) {
405
333
  '`dangerouslyAllowBrowser` option to `true`, e.g.,\n\n' +
406
334
  ' Ablo({ schema, apiKey, dangerouslyAllowBrowser: true });\n', { code: 'browser_apikey_blocked' });
407
335
  }
408
- // `databaseUrl` carries database credentials and is never browser-safe, so
409
- // `dangerouslyAllowBrowser` does not override this check. Register your
410
- // database from a server-side runtime.
411
- if (inBrowser && typeof input.databaseUrl === 'string' && input.databaseUrl.length > 0) {
412
- throw new AbloAuthenticationError('Ablo `databaseUrl` cannot be used in a browser-like environment — it ' +
413
- 'carries your database credentials. Initialize the client with ' +
414
- '`databaseUrl` from a server-side runtime only.', { code: 'browser_database_url_blocked' });
415
- }
416
336
  }
417
337
  /**
418
338
  * Resolves an {@link ApiKeySetter} callable to its current string value, or
@@ -20,7 +20,7 @@ import type { OnDemandLoader } from '../sync/OnDemandLoader.js';
20
20
  import type { JoinedParticipant } from '../sync/participants.js';
21
21
  import type { LoadWhere } from '../query/types.js';
22
22
  import { ModelScope } from '../types/index.js';
23
- import type { Duration, Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, ClaimWaitOptions, Snapshot, TargetRange } from '../types/streams.js';
23
+ import type { Duration, Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, HeldLease, ClaimWaitOptions, Snapshot, TargetRange } from '../types/streams.js';
24
24
  export interface ModelClientMeta {
25
25
  readonly key: string;
26
26
  readonly typename: string;
@@ -88,8 +88,8 @@ export interface ModelCollaboration<T> {
88
88
  range?: TargetRange;
89
89
  meta?: Record<string, unknown>;
90
90
  };
91
- /** Human-readable phase (`'editing'`). */
92
- reason: string;
91
+ /** Peer-visible description of the work (`'rewriting the risk section'`). */
92
+ description?: string;
93
93
  ttl?: Duration;
94
94
  /**
95
95
  * Block on the server's fair FIFO queue when the target is held, rather
@@ -179,10 +179,9 @@ export interface ModelCollaboration<T> {
179
179
  createWatch?(modelKey: string, ids: string | readonly string[], options?: WatchOptions): Promise<JoinedParticipant>;
180
180
  }
181
181
  export interface ClaimTargetOptions<T = Record<string, unknown>> {
182
- /** Human-readable phase shown to observers while held. Defaults to
183
- * `'editing'`. The same word on every claim surface. */
184
- reason?: string;
185
- /** Peer-visible explanation of the work being performed. */
182
+ /** Peer-visible description of the work being performed the sentence a
183
+ * contending participant reads to decide whether to wait, work elsewhere, or
184
+ * move on. Defaults to `'editing'`. The same field on every claim surface. */
186
185
  description?: string;
187
186
  /** Field-level target, for fine-grained claimed-state badges. */
188
187
  field?: string;
@@ -257,7 +256,6 @@ export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLo
257
256
  * ```ts
258
257
  * const claim = await ablo.weatherReports.claim({
259
258
  * id: 'report_stockholm',
260
- * reason: 'forecasting',
261
259
  * description: 'Fetching current weather before writing the forecast.',
262
260
  * });
263
261
  * try {
@@ -275,7 +273,7 @@ export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLo
275
273
  * `ablo.<model>.update({ id, data, claim })` verb — the handle carries the
276
274
  * lease id and snapshot watermark for attribution and stale-write protection.
277
275
  */
278
- export type { Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim };
276
+ export type { Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, HeldLease };
279
277
  export type ClaimOptions<T = Record<string, unknown>> = ClaimTargetOptions<T>;
280
278
  /**
281
279
  * The coordination surface for a model, exposed as a callable namespace.
@@ -289,7 +287,6 @@ export type ClaimOptions<T = Record<string, unknown>> = ClaimTargetOptions<T>;
289
287
  * data: { title },
290
288
  * claim: {
291
289
  * field: 'title',
292
- * reason: 'renaming',
293
290
  * description: 'Renaming the task to match the project brief.',
294
291
  * },
295
292
  * });
@@ -345,6 +342,15 @@ export interface ClaimApi<T> extends ClaimReadApi<T> {
345
342
  * directly and `await using` works without a guard.
346
343
  */
347
344
  (params: ClaimParams<T>): Promise<HeldClaim<T>>;
345
+ /**
346
+ * Takes a claim by id alone, for a row that lives only in the customer's own
347
+ * database — Ablo has never seen it, so there is nothing to re-read. Returns a
348
+ * {@link HeldLease}: the same lease controls as {@link HeldClaim}
349
+ * (`release`, `revoke`, `heartbeat`, `await using`) but no `.data`. Locking a
350
+ * key you know by id is exactly this — serialize writers without first
351
+ * syncing the row into Ablo.
352
+ */
353
+ (id: string, opts?: ClaimOptions<T>): Promise<HeldLease>;
348
354
  }
349
355
  export interface ModelRetrieveParams extends ServerRetrieveOptions {
350
356
  readonly id: string;
@@ -440,7 +446,6 @@ export interface ModelOperations<T, CreateInput> {
440
446
  * ```ts
441
447
  * const claim = await ablo.weatherReports.claim({
442
448
  * id: 'report_stockholm',
443
- * reason: 'forecasting',
444
449
  * description: 'Fetching fresh weather before updating the report.',
445
450
  * });
446
451
  * const weather = await getWeather(claim.data.location);