@abloatai/ablo 0.17.0 → 0.19.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 (64) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/BaseSyncedStore.d.ts +5 -4
  3. package/dist/BaseSyncedStore.js +38 -14
  4. package/dist/NetworkMonitor.js +3 -1
  5. package/dist/ObjectPool.d.ts +14 -1
  6. package/dist/ObjectPool.js +8 -1
  7. package/dist/SyncClient.js +7 -1
  8. package/dist/SyncEngineContext.js +2 -0
  9. package/dist/ai-sdk/coordination-context.d.ts +2 -1
  10. package/dist/ai-sdk/coordination-context.js +3 -3
  11. package/dist/ai-sdk/index.d.ts +3 -4
  12. package/dist/ai-sdk/index.js +2 -3
  13. package/dist/ai-sdk/wrap.d.ts +13 -18
  14. package/dist/ai-sdk/wrap.js +2 -6
  15. package/dist/cli.cjs +459 -205
  16. package/dist/client/Ablo.d.ts +116 -23
  17. package/dist/client/Ablo.js +128 -38
  18. package/dist/client/ApiClient.d.ts +2 -2
  19. package/dist/client/ApiClient.js +27 -32
  20. package/dist/client/auth.d.ts +26 -0
  21. package/dist/client/auth.js +209 -1
  22. package/dist/client/createModelProxy.d.ts +16 -11
  23. package/dist/client/createModelProxy.js +15 -15
  24. package/dist/client/sessionMint.d.ts +10 -0
  25. package/dist/client/sessionMint.js +8 -1
  26. package/dist/coordination/schema.d.ts +10 -10
  27. package/dist/coordination/schema.js +7 -8
  28. package/dist/coordination/trace.d.ts +91 -0
  29. package/dist/coordination/trace.js +147 -0
  30. package/dist/core/StoreManager.js +3 -1
  31. package/dist/errorCodes.d.ts +2 -0
  32. package/dist/errorCodes.js +2 -0
  33. package/dist/errors.d.ts +10 -2
  34. package/dist/errors.js +20 -9
  35. package/dist/index.d.ts +7 -1
  36. package/dist/index.js +12 -0
  37. package/dist/interfaces/index.d.ts +45 -2
  38. package/dist/policy/types.d.ts +33 -9
  39. package/dist/policy/types.js +42 -11
  40. package/dist/react/AbloProvider.d.ts +2 -2
  41. package/dist/schema/ddl.d.ts +36 -0
  42. package/dist/schema/ddl.js +66 -0
  43. package/dist/schema/index.d.ts +1 -1
  44. package/dist/schema/index.js +1 -1
  45. package/dist/surface.d.ts +1 -1
  46. package/dist/surface.js +2 -0
  47. package/dist/sync/HydrationCoordinator.js +7 -3
  48. package/dist/sync/SyncWebSocket.d.ts +11 -2
  49. package/dist/sync/SyncWebSocket.js +68 -4
  50. package/dist/sync/awaitClaimGrant.js +9 -2
  51. package/dist/sync/createClaimStream.d.ts +9 -2
  52. package/dist/sync/createClaimStream.js +41 -7
  53. package/dist/sync/participants.d.ts +12 -11
  54. package/dist/sync/participants.js +5 -5
  55. package/dist/transactions/TransactionQueue.d.ts +1 -0
  56. package/dist/transactions/TransactionQueue.js +40 -3
  57. package/dist/types/streams.d.ts +57 -135
  58. package/dist/wire/errorEnvelope.d.ts +13 -0
  59. package/docs/coordination.md +16 -0
  60. package/docs/debugging.md +194 -0
  61. package/docs/index.md +1 -0
  62. package/package.json +1 -1
  63. package/dist/ai-sdk/claim-broadcast.d.ts +0 -83
  64. package/dist/ai-sdk/claim-broadcast.js +0 -79
@@ -6,23 +6,11 @@
6
6
  * nouns directly to HTTP routes on sync-server.
7
7
  */
8
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';
9
+ import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfCliKeyMismatch, warnIfDatabaseUrlEnvIgnored, warnIfDatabaseUrlDeprecated, } from './auth.js';
10
10
  import { registerDataSource } from './registerDataSource.js';
11
11
  import { toSeconds } from '../utils/duration.js';
12
12
  import { mintSession } from './sessionMint.js';
13
13
  import { assertWriteOptions } from './writeOptionsSchema.js';
14
- /**
15
- * The `/v1/claims` and model-query routes still emit the wire field `action`
16
- * for the claim phase; the public `Claim` / `ModelClaim` expose it as `reason`.
17
- * Heal on read so the SDK shape is consistent without a coordinated server
18
- * deploy — `reason ?? action`. When the server adopts `reason`, this is a no-op.
19
- */
20
- function healClaimPhase(claim) {
21
- const raw = claim;
22
- if (raw.reason !== undefined)
23
- return claim;
24
- return { ...claim, reason: raw.action ?? 'editing' };
25
- }
26
14
  const DEFAULT_AGENT_LEASE = '10m';
27
15
  export function createProtocolClient(options) {
28
16
  const env = readProcessEnv();
@@ -33,6 +21,8 @@ export function createProtocolClient(options) {
33
21
  // Nudge (once) if a stray DATABASE_URL is in the env but `databaseUrl` wasn't
34
22
  // passed — no logger on this path, so the helper falls back to console.warn.
35
23
  warnIfDatabaseUrlEnvIgnored(authInput);
24
+ warnIfDatabaseUrlDeprecated(authInput);
25
+ void warnIfCliKeyMismatch(authInput);
36
26
  assertBrowserSafety({
37
27
  apiKey: configuredApiKey,
38
28
  databaseUrl: configuredDatabaseUrl,
@@ -185,8 +175,8 @@ export function createProtocolClient(options) {
185
175
  const suffix = params.toString();
186
176
  const body = await requestJson(`/v1/claims${suffix ? `?${suffix}` : ''}`, { method: 'GET' });
187
177
  return {
188
- active: (body.claims ?? []).map(healClaimPhase),
189
- queue: (body.queue ?? []).map(healClaimPhase),
178
+ active: body.claims ?? [],
179
+ queue: body.queue ?? [],
190
180
  };
191
181
  }
192
182
  function delay(ms, signal) {
@@ -272,7 +262,7 @@ export function createProtocolClient(options) {
272
262
  body: JSON.stringify({
273
263
  clientTxId,
274
264
  idempotencyKey: clientTxId,
275
- claim: normalizeClaimId(commitOptions.claimRef) ?? claim?.claimId,
265
+ claim: normalizeClaimId(commitOptions.claimRef) ?? claim?.id,
276
266
  operations,
277
267
  }),
278
268
  });
@@ -389,8 +379,7 @@ export function createProtocolClient(options) {
389
379
  body: JSON.stringify({
390
380
  claimId,
391
381
  target: claimOptions.target,
392
- // Wire field stays `action`; public option is `reason`.
393
- action: claimOptions.reason,
382
+ reason: claimOptions.reason,
394
383
  ttl: claimOptions.ttl,
395
384
  queue: claimOptions.queue,
396
385
  }),
@@ -415,9 +404,16 @@ export function createProtocolClient(options) {
415
404
  };
416
405
  return {
417
406
  object: 'claim',
418
- claimId: id,
407
+ id,
419
408
  reason: claimOptions.reason,
420
- target: claimOptions.target,
409
+ target: {
410
+ type: claimOptions.target.model,
411
+ id: claimOptions.target.id,
412
+ ...(claimOptions.target.field ? { field: claimOptions.target.field } : {}),
413
+ ...(claimOptions.target.path ? { path: claimOptions.target.path } : {}),
414
+ ...(claimOptions.target.range ? { range: claimOptions.target.range } : {}),
415
+ ...(claimOptions.target.meta ? { meta: claimOptions.target.meta } : {}),
416
+ },
421
417
  release,
422
418
  revoke: () => {
423
419
  void release().catch(() => { });
@@ -467,7 +463,7 @@ export function createProtocolClient(options) {
467
463
  return {
468
464
  data,
469
465
  stamp: query.stamp ?? 0,
470
- claims: (query.claims ?? []).map(healClaimPhase),
466
+ claims: query.claims ?? [],
471
467
  };
472
468
  }
473
469
  /**
@@ -501,13 +497,13 @@ export function createProtocolClient(options) {
501
497
  const claimHandle = typeof options?.claim === 'object' &&
502
498
  options?.claim !== null &&
503
499
  options.claim.object === 'claim' &&
504
- typeof options.claim.claimId === 'string'
500
+ typeof options.claim.id === 'string'
505
501
  ? options.claim
506
502
  : undefined;
507
503
  const readAt = options?.readAt ?? claimHandle?.readAt;
508
504
  const requestBody = {
509
505
  idempotencyKey: clientTxId,
510
- claim: normalizeClaimId(options?.claimRef) ?? claimHandle?.claimId,
506
+ claim: normalizeClaimId(options?.claimRef) ?? claimHandle?.id,
511
507
  onStale: options?.onStale ?? (claimHandle?.readAt !== undefined ? 'reject' : undefined),
512
508
  readAt,
513
509
  };
@@ -538,7 +534,7 @@ export function createProtocolClient(options) {
538
534
  const isClaimHandle = (value) => typeof value === 'object' &&
539
535
  value !== null &&
540
536
  value.object === 'claim' &&
541
- typeof value.claimId === 'string' &&
537
+ typeof value.id === 'string' &&
542
538
  typeof value.release === 'function';
543
539
  const claimMeta = (options) => {
544
540
  if (!options?.description)
@@ -549,8 +545,7 @@ export function createProtocolClient(options) {
549
545
  const body = await requestJson(claimPath(params.id), {
550
546
  method: 'POST',
551
547
  body: JSON.stringify({
552
- // Wire field stays `action`; public option is `reason`.
553
- action: params.reason ?? 'editing',
548
+ reason: params.reason ?? 'editing',
554
549
  ...(params.ttl !== undefined ? { ttl: params.ttl } : {}),
555
550
  ...(params.description !== undefined ? { description: params.description } : {}),
556
551
  ...(claimMeta(params) ? { meta: claimMeta(params) } : {}),
@@ -563,7 +558,7 @@ export function createProtocolClient(options) {
563
558
  throw new AbloClaimedError(`Target ${name}/${params.id} is held; queued at position ${body.position ?? 0}. ` +
564
559
  `The HTTP client cannot await the grant without a WebSocket.`, { code: 'claim_queued' });
565
560
  }
566
- return body.claim?.id ?? body.id ?? body.claimId ?? createClaimId();
561
+ return body.claim?.id ?? body.id ?? body.id ?? createClaimId();
567
562
  };
568
563
  const releaseClaim = (params) => requestJson(claimPath(isClaimHandle(params) ? params.target.id : params.id), { method: 'DELETE' }).then(() => undefined);
569
564
  async function claimImpl(params) {
@@ -572,10 +567,10 @@ export function createProtocolClient(options) {
572
567
  const release = () => releaseClaim(params);
573
568
  return {
574
569
  object: 'claim',
575
- claimId,
570
+ id: claimId,
576
571
  readAt: stamp,
577
572
  target: {
578
- model: name,
573
+ type: name,
579
574
  id: params.id,
580
575
  ...(params.field ? { field: params.field } : {}),
581
576
  ...(params.path ? { path: params.path } : {}),
@@ -598,11 +593,11 @@ export function createProtocolClient(options) {
598
593
  state: async (params) => {
599
594
  const res = await claimsForEntity(params);
600
595
  const first = res.claims?.[0];
601
- return first ? healClaimPhase(first) : null;
596
+ return first ?? null;
602
597
  },
603
598
  queue: async (params) => {
604
599
  const res = await claimsForEntity(params);
605
- return { object: 'list', data: (res.queue ?? []).map(healClaimPhase) };
600
+ return { object: 'list', data: res.queue ?? [] };
606
601
  },
607
602
  reorder: async (params) => {
608
603
  await requestJson(`${claimPath(params.id)}/reorder`, {
@@ -618,7 +613,7 @@ export function createProtocolClient(options) {
618
613
  if (!claimInput)
619
614
  return run(input);
620
615
  if (isClaimHandle(claimInput)) {
621
- return run({ ...input, claimRef: { id: claimInput.claimId }, claim: undefined });
616
+ return run({ ...input, claimRef: { id: claimInput.id }, claim: undefined });
622
617
  }
623
618
  const claimId = await acquireClaim({ id, ...claimInput });
624
619
  try {
@@ -49,6 +49,29 @@ export interface AuthResolveInput {
49
49
  export declare function readProcessEnv(): Record<string, string | undefined>;
50
50
  export declare function resolveApiKey(input: AuthResolveInput): string | ApiKeySetter | null;
51
51
  export declare function resolveAuthToken(input: AuthResolveInput): string | null;
52
+ type CliMode = 'sandbox' | 'production';
53
+ type StaticApiKeySource = 'option' | 'env';
54
+ interface StaticApiKey {
55
+ readonly key: string;
56
+ readonly source: StaticApiKeySource;
57
+ }
58
+ interface CliCredentialSnapshot {
59
+ readonly mode: CliMode;
60
+ readonly activeProfile: string;
61
+ readonly storedKey?: string;
62
+ }
63
+ export interface CliKeyMismatch {
64
+ readonly source: StaticApiKeySource;
65
+ readonly configuredKeyPrefix: string;
66
+ readonly configuredMode?: CliMode;
67
+ readonly cliMode: CliMode;
68
+ readonly storedKeyPrefix?: string;
69
+ readonly kind: 'mode_mismatch' | 'key_override';
70
+ readonly message: string;
71
+ }
72
+ /** Infer sandbox/production from Ablo key prefixes without importing CLI code. */
73
+ export declare function modeFromApiKey(key: string): CliMode | undefined;
74
+ export declare function describeCliKeyMismatch(configured: StaticApiKey, cli: CliCredentialSnapshot): CliKeyMismatch | null;
52
75
  /**
53
76
  * Resolve the direct-URL connector's Postgres connection string.
54
77
  *
@@ -63,6 +86,8 @@ export declare function resolveAuthToken(input: AuthResolveInput): string | null
63
86
  */
64
87
  export declare function resolveDatabaseUrl(input: AuthResolveInput): string | null;
65
88
  export declare function warnIfDatabaseUrlEnvIgnored(input: AuthResolveInput, warn?: (message: string) => void): void;
89
+ export declare function warnIfDatabaseUrlDeprecated(input: AuthResolveInput, warn?: (message: string) => void): void;
90
+ export declare function warnIfCliKeyMismatch(input: AuthResolveInput, warn?: (message: string) => void): Promise<void>;
66
91
  export declare const ABLO_HOSTED_API_DOMAIN = "api.abloatai.com";
67
92
  export declare const ABLO_HOSTED_HTTP_BASE_URL = "https://api.abloatai.com";
68
93
  export declare const ABLO_DEFAULT_BASE_URL = "https://api.abloatai.com";
@@ -108,3 +133,4 @@ export declare function resolveBootstrapBaseUrl(input: {
108
133
  readonly url: string;
109
134
  readonly bootstrapBaseUrl?: string;
110
135
  }): string;
136
+ export {};
@@ -28,6 +28,160 @@ export function resolveApiKey(input) {
28
28
  export function resolveAuthToken(input) {
29
29
  return input.options.authToken ?? null;
30
30
  }
31
+ function keyPrefix(key) {
32
+ return `${key.slice(0, 12)}…`;
33
+ }
34
+ /** Infer sandbox/production from Ablo key prefixes without importing CLI code. */
35
+ export function modeFromApiKey(key) {
36
+ if (/^(sk|rk)_test_/.test(key))
37
+ return 'sandbox';
38
+ if (/^(sk|rk)_live_/.test(key))
39
+ return 'production';
40
+ return undefined;
41
+ }
42
+ function resolveStaticApiKey(input) {
43
+ if (typeof input.options.apiKey === 'string') {
44
+ return { key: input.options.apiKey, source: 'option' };
45
+ }
46
+ if (input.options.apiKey !== undefined && input.options.apiKey !== null) {
47
+ return null;
48
+ }
49
+ const envKey = input.env.ABLO_API_KEY;
50
+ if (typeof envKey === 'string' && envKey.length > 0) {
51
+ return { key: envKey, source: 'env' };
52
+ }
53
+ return null;
54
+ }
55
+ function readProfileKeys(value) {
56
+ if (!value || typeof value !== 'object')
57
+ return {};
58
+ const profiles = {};
59
+ for (const [name, raw] of Object.entries(value)) {
60
+ if (!raw || typeof raw !== 'object')
61
+ continue;
62
+ const row = raw;
63
+ const sandbox = row.sandbox;
64
+ const production = row.production;
65
+ profiles[name] = {
66
+ sandbox: sandbox && typeof sandbox === 'object' ? sandbox : undefined,
67
+ production: production && typeof production === 'object' ? production : undefined,
68
+ };
69
+ }
70
+ return profiles;
71
+ }
72
+ function legacyProfileKeys(value) {
73
+ if (!value)
74
+ return { sandbox: undefined, production: undefined };
75
+ const sandbox = value.sandbox;
76
+ const production = value.production;
77
+ if ((sandbox && typeof sandbox === 'object') ||
78
+ (production && typeof production === 'object')) {
79
+ return {
80
+ sandbox: sandbox && typeof sandbox === 'object' ? sandbox : undefined,
81
+ production: production && typeof production === 'object' ? production : undefined,
82
+ };
83
+ }
84
+ if (typeof value.apiKey === 'string') {
85
+ return { sandbox: { apiKey: value.apiKey }, production: undefined };
86
+ }
87
+ return { sandbox: undefined, production: undefined };
88
+ }
89
+ function normalizeCliMode(value) {
90
+ return value === 'sandbox' || value === 'production' ? value : undefined;
91
+ }
92
+ function activeProjectSlug(value) {
93
+ if (!value || typeof value !== 'object')
94
+ return undefined;
95
+ const slug = value.slug;
96
+ return typeof slug === 'string' && slug.length > 0 ? slug : undefined;
97
+ }
98
+ function importNodeBuiltin(specifier) {
99
+ return import(specifier);
100
+ }
101
+ async function readJsonIfPresent(path) {
102
+ try {
103
+ const { readFile } = await importNodeBuiltin('node:fs/promises');
104
+ const text = await readFile(path, 'utf8');
105
+ const parsed = JSON.parse(text);
106
+ return parsed && typeof parsed === 'object' ? parsed : null;
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ async function readCliCredentialSnapshot(env) {
113
+ const processLike = globalThis.process;
114
+ if (!processLike?.versions?.node)
115
+ return null;
116
+ if (typeof window !== 'undefined')
117
+ return null;
118
+ const [{ homedir }, { join }] = await Promise.all([
119
+ importNodeBuiltin('node:os'),
120
+ importNodeBuiltin('node:path'),
121
+ ]);
122
+ const dir = env.ABLO_CONFIG_DIR
123
+ ?? (env.XDG_CONFIG_HOME ? join(env.XDG_CONFIG_HOME, 'ablo') : join(homedir(), '.config', 'ablo'));
124
+ const [cfg, creds] = await Promise.all([
125
+ readJsonIfPresent(join(dir, 'config.json')),
126
+ readJsonIfPresent(join(dir, 'credentials.json')),
127
+ ]);
128
+ const mode = normalizeCliMode(cfg?.mode) ?? normalizeCliMode(creds?.mode);
129
+ const activeProfile = activeProjectSlug(cfg?.activeProject) ?? 'default';
130
+ const profiles = {
131
+ ...readProfileKeys(creds?.profiles),
132
+ ...readProfileKeys(cfg?.profiles),
133
+ };
134
+ if (!profiles[activeProfile]) {
135
+ const legacy = { ...legacyProfileKeys(cfg), ...legacyProfileKeys(creds) };
136
+ if (legacy.sandbox?.apiKey || legacy.production?.apiKey) {
137
+ profiles[activeProfile] = legacy;
138
+ }
139
+ }
140
+ const effectiveMode = mode ?? (Object.keys(profiles).length > 0 ? 'sandbox' : undefined);
141
+ if (!effectiveMode)
142
+ return null;
143
+ return {
144
+ mode: effectiveMode,
145
+ activeProfile,
146
+ ...(profiles[activeProfile]?.[effectiveMode]?.apiKey
147
+ ? { storedKey: profiles[activeProfile][effectiveMode].apiKey }
148
+ : {}),
149
+ };
150
+ }
151
+ export function describeCliKeyMismatch(configured, cli) {
152
+ const configuredMode = modeFromApiKey(configured.key);
153
+ const configuredKeyPrefix = keyPrefix(configured.key);
154
+ const storedKeyPrefix = cli.storedKey ? keyPrefix(cli.storedKey) : undefined;
155
+ const sourceLabel = configured.source === 'env' ? 'ABLO_API_KEY' : 'configured apiKey';
156
+ if (configuredMode && configuredMode !== cli.mode) {
157
+ return {
158
+ source: configured.source,
159
+ configuredKeyPrefix,
160
+ configuredMode,
161
+ cliMode: cli.mode,
162
+ ...(storedKeyPrefix ? { storedKeyPrefix } : {}),
163
+ kind: 'mode_mismatch',
164
+ message: `${sourceLabel} is a ${configuredMode} key (${configuredKeyPrefix}) but the Ablo CLI is in ` +
165
+ `${cli.mode} mode${storedKeyPrefix ? ` (active stored key ${storedKeyPrefix})` : ''}. ` +
166
+ `Requests will use ${configuredMode}. Use the ${cli.mode} key, unset ABLO_API_KEY, ` +
167
+ `or run \`ablo mode ${configuredMode}\` intentionally.`,
168
+ };
169
+ }
170
+ if (configured.source === 'env' && cli.storedKey && configured.key !== cli.storedKey) {
171
+ return {
172
+ source: configured.source,
173
+ configuredKeyPrefix,
174
+ ...(configuredMode ? { configuredMode } : {}),
175
+ cliMode: cli.mode,
176
+ storedKeyPrefix,
177
+ kind: 'key_override',
178
+ message: `ABLO_API_KEY (${configuredKeyPrefix}) overrides the CLI's stored active ${cli.mode} key ` +
179
+ `(${storedKeyPrefix}). Requests will use the environment key. Unset ABLO_API_KEY to use ` +
180
+ '`ablo status` / `ablo mode` credentials.',
181
+ };
182
+ }
183
+ return null;
184
+ }
31
185
  /**
32
186
  * Resolve the direct-URL connector's Postgres connection string.
33
187
  *
@@ -83,7 +237,61 @@ export function warnIfDatabaseUrlEnvIgnored(input, warn) {
83
237
  if (warn)
84
238
  warn(message);
85
239
  else if (typeof console !== 'undefined')
86
- console.warn('[sync]', message);
240
+ console.warn('[Ablo]', message);
241
+ }
242
+ /**
243
+ * One-time deprecation nudge for the `databaseUrl` direct connector.
244
+ *
245
+ * `databaseUrl` registers the `dedicated` storage mode — Ablo opens a pool INTO
246
+ * the caller's Postgres and writes into it directly. That is the operate-their-
247
+ * database posture we are moving off. Ablo is Stripe-shaped: it hosts only the
248
+ * transaction log (the ordered sync_deltas) + coordination, never your data — your
249
+ * rows always live in your own database. The supported path is the signed Data
250
+ * Source endpoint (`dataSource(...)`), where your app owns the write and your
251
+ * credentials never leave it. See docs/plans/stripe-shaped-storage-posture.md.
252
+ *
253
+ * Still honored at runtime so existing integrations keep working; this only warns
254
+ * once per process (so it never spams) and falls back to `console.warn` when no
255
+ * logger is supplied (the `transport: 'http'`/`'api'` client has none).
256
+ */
257
+ let warnedDatabaseUrlDeprecated = false;
258
+ export function warnIfDatabaseUrlDeprecated(input, warn) {
259
+ if (warnedDatabaseUrlDeprecated)
260
+ return;
261
+ if (input.options.databaseUrl == null)
262
+ return;
263
+ warnedDatabaseUrlDeprecated = true;
264
+ const message = '`databaseUrl` (the direct connector) is deprecated and will be removed from ' +
265
+ 'the supported path. It lets Ablo dial into your database; we are moving off ' +
266
+ 'that. Ablo hosts only the transaction log — your data stays in your DB. Expose ' +
267
+ 'a signed Data Source endpoint (`dataSource(...)`) so your app owns the write, ' +
268
+ 'or self-host the engine to keep the log in your infra too. ' +
269
+ 'See docs/plans/stripe-shaped-storage-posture.md.';
270
+ if (warn)
271
+ warn(message);
272
+ else if (typeof console !== 'undefined')
273
+ console.warn('[Ablo]', message);
274
+ }
275
+ let warnedCliKeyMismatch = false;
276
+ export async function warnIfCliKeyMismatch(input, warn) {
277
+ if (warnedCliKeyMismatch)
278
+ return;
279
+ if (input.env.NODE_ENV === 'production')
280
+ return;
281
+ const configured = resolveStaticApiKey(input);
282
+ if (!configured)
283
+ return;
284
+ const cli = await readCliCredentialSnapshot(input.env);
285
+ if (!cli)
286
+ return;
287
+ const mismatch = describeCliKeyMismatch(configured, cli);
288
+ if (!mismatch)
289
+ return;
290
+ warnedCliKeyMismatch = true;
291
+ if (warn)
292
+ warn(mismatch.message);
293
+ else if (typeof console !== 'undefined')
294
+ console.warn('[Ablo]', mismatch.message);
87
295
  }
88
296
  export const ABLO_HOSTED_API_DOMAIN = 'api.abloatai.com';
89
297
  export const ABLO_HOSTED_HTTP_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
@@ -22,7 +22,7 @@ import type { HydrationCoordinator } from '../sync/HydrationCoordinator.js';
22
22
  import type { JoinedParticipant } from '../sync/participants.js';
23
23
  import type { LoadWhere } from '../query/types.js';
24
24
  import { ModelScope } from '../types/index.js';
25
- import type { Duration, Claim, ClaimHandle, ClaimWaitOptions, Snapshot, TargetRange } from '../types/streams.js';
25
+ import type { Duration, Claim, ClaimWaitOptions, Snapshot, TargetRange } from '../types/streams.js';
26
26
  export interface ModelClientMeta {
27
27
  readonly key: string;
28
28
  readonly typename: string;
@@ -90,7 +90,7 @@ export interface ModelCollaboration<T> {
90
90
  range?: TargetRange;
91
91
  meta?: Record<string, unknown>;
92
92
  };
93
- /** Human-readable phase (`'editing'`); wire field is `action`. */
93
+ /** Human-readable phase (`'editing'`). */
94
94
  reason: string;
95
95
  ttl?: Duration;
96
96
  /**
@@ -101,7 +101,7 @@ export interface ModelCollaboration<T> {
101
101
  queue?: boolean;
102
102
  /** Reject (don't wait) if the queue is already this deep when we join. */
103
103
  maxQueueDepth?: number;
104
- }): Promise<ClaimHandle>;
104
+ }): Promise<Claim>;
105
105
  createSnapshot(modelKey: string, id: string): Snapshot;
106
106
  /**
107
107
  * Current coordination state on a target — who (if anyone) holds it.
@@ -185,7 +185,7 @@ export interface ModelCollaboration<T> {
185
185
  }
186
186
  export interface ClaimTargetOptions<T = Record<string, unknown>> {
187
187
  /** Human-readable phase shown to observers while held. Defaults to
188
- * `'editing'`. The same word on every claim surface; wire field is `action`. */
188
+ * `'editing'`. The same word on every claim surface. */
189
189
  reason?: string;
190
190
  /** Peer-visible explanation of the work being performed. */
191
191
  description?: string;
@@ -258,7 +258,7 @@ export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLo
258
258
  * `ablo.<model>.update({ id, data, claim })` verb — the handle carries the
259
259
  * lease id and snapshot watermark for attribution + stale protection.
260
260
  */
261
- export type { ClaimHandle };
261
+ export type { Claim };
262
262
  export type ClaimOptions<T = Record<string, unknown>> = ClaimTargetOptions<T>;
263
263
  /**
264
264
  * The coordination surface for a model, exposed as a callable namespace.
@@ -312,7 +312,7 @@ export interface ClaimReadApi<T = Record<string, unknown>> {
312
312
  */
313
313
  reorder(params: ClaimReorderParams<T>): void;
314
314
  /** Release a manual claim handle early. Single-write claims auto-release. */
315
- release(params: ClaimLookupParams<T> | ClaimHandle<T>): Promise<void>;
315
+ release(params: ClaimLookupParams<T> | Claim<T>): Promise<void>;
316
316
  }
317
317
  /**
318
318
  * The awaited form of a claim method: a synchronous return becomes a `Promise`,
@@ -321,8 +321,13 @@ export interface ClaimReadApi<T = Record<string, unknown>> {
321
321
  */
322
322
  export type AwaitedClaimMethod<F> = F extends (...args: infer A) => infer R ? R extends Promise<unknown> ? (...args: A) => R : (...args: A) => Promise<R> : F;
323
323
  export interface ClaimApi<T> extends ClaimReadApi<T> {
324
- /** Take a claim and get an explicit held-work handle back. */
325
- (params: ClaimParams<T>): Promise<ClaimHandle<T>>;
324
+ /**
325
+ * Take a claim and get an explicit held-work handle back. Returns a
326
+ * {@link Claim} — `data` (and `readAt`) are guaranteed present
327
+ * because this door always re-reads the row under the lease, so callers use
328
+ * `handle.data` directly without a guard.
329
+ */
330
+ (params: ClaimParams<T>): Promise<Claim<T>>;
326
331
  }
327
332
  export interface ModelRetrieveParams extends ServerRetrieveOptions {
328
333
  readonly id: string;
@@ -330,16 +335,16 @@ export interface ModelRetrieveParams extends ServerRetrieveOptions {
330
335
  export interface ModelCreateParams<T, CreateInput> extends MutationOptions {
331
336
  readonly data: CreateInput;
332
337
  readonly id?: string | null;
333
- readonly claim?: ClaimHandle<T> | ClaimTargetOptions<T> | null;
338
+ readonly claim?: Claim<T> | ClaimTargetOptions<T> | null;
334
339
  }
335
340
  export interface ModelUpdateParams<T> extends MutationOptions {
336
341
  readonly id: string;
337
342
  readonly data: Partial<T>;
338
- readonly claim?: ClaimHandle<T> | ClaimTargetOptions<T> | null;
343
+ readonly claim?: Claim<T> | ClaimTargetOptions<T> | null;
339
344
  }
340
345
  export interface ModelDeleteParams<T> extends MutationOptions {
341
346
  readonly id: string;
342
- readonly claim?: ClaimHandle<T> | ClaimTargetOptions<T> | null;
347
+ readonly claim?: Claim<T> | ClaimTargetOptions<T> | null;
343
348
  }
344
349
  /** Options for the WebSocket-only `ablo.<model>.watch(ids, options?)`. */
345
350
  export interface WatchOptions {
@@ -39,7 +39,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
39
39
  // with the commit op's model name; a lease recorded under the schema key
40
40
  // never collides with it — which silently disarmed the guard for every
41
41
  // model whose schema key differs from its typename (i.e. nearly all of
42
- // them, plural key vs singular typename). Public surfaces (ClaimHandle.
42
+ // them, plural key vs singular typename). Public surfaces (Claim.
43
43
  // target.model) keep the schema key; only the wire/coordination targets
44
44
  // use this.
45
45
  const wireModel = registeredModelName.toLowerCase();
@@ -90,7 +90,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
90
90
  const isClaimHandle = (value) => typeof value === 'object' &&
91
91
  value !== null &&
92
92
  value.object === 'claim' &&
93
- typeof value.claimId === 'string' &&
93
+ typeof value.id === 'string' &&
94
94
  typeof value.release === 'function';
95
95
  const claimMeta = (options) => {
96
96
  if (!options?.description)
@@ -131,7 +131,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
131
131
  if (!held)
132
132
  return;
133
133
  activeClaims.delete(id);
134
- await held.lease.release();
134
+ await held.lease.release?.();
135
135
  };
136
136
  const takeClaim = async (params) => {
137
137
  if (!collaboration) {
@@ -214,7 +214,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
214
214
  }
215
215
  const snapshot = collaboration.createSnapshot(schemaKey, id);
216
216
  const reason = options?.reason ?? 'editing';
217
- // The self-claim's `EntityRef` mirrors what a peer's `claim.state` would
217
+ // The self-claim's `ClaimTarget` mirrors what a peer's `claim.state` would
218
218
  // report (`state` maps `held.target.model` → `type`), so a holder and a
219
219
  // peer see the SAME target.type for one row — the wire model token.
220
220
  const selfTarget = {
@@ -235,7 +235,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
235
235
  expiresAt,
236
236
  });
237
237
  const target = {
238
- model: schemaKey,
238
+ type: schemaKey,
239
239
  id,
240
240
  ...(options?.field ? { field: options.field } : {}),
241
241
  ...(options?.path ? { path: options.path } : {}),
@@ -245,7 +245,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
245
245
  const release = () => releaseClaim(id);
246
246
  return {
247
247
  object: 'claim',
248
- claimId: lease.claimId,
248
+ id: lease.id,
249
249
  readAt: snapshot.stamp,
250
250
  target,
251
251
  reason,
@@ -278,7 +278,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
278
278
  if (own) {
279
279
  return {
280
280
  object: 'claim',
281
- id: own.lease.claimId,
281
+ id: own.lease.id,
282
282
  status: 'active',
283
283
  target: own.target,
284
284
  reason: own.reason,
@@ -403,7 +403,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
403
403
  const effective = {
404
404
  ...opts,
405
405
  ...(autoLease ? { claim: autoLease } : {}),
406
- ...(isClaimHandle(claim) ? { claim: { id: claim.claimId } } : {}),
406
+ ...(isClaimHandle(claim) ? { claim: { id: claim.id } } : {}),
407
407
  };
408
408
  try {
409
409
  syncClient.add(model, effective);
@@ -411,7 +411,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
411
411
  return modelAsRow(model);
412
412
  }
413
413
  finally {
414
- await autoLease?.release().catch(() => { });
414
+ await autoLease?.release?.().catch(() => { });
415
415
  }
416
416
  }),
417
417
  update: guard(async (params) => {
@@ -422,7 +422,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
422
422
  return await operations.update({ ...params, claim: handle });
423
423
  }
424
424
  finally {
425
- await handle.release();
425
+ await handle.release?.();
426
426
  }
427
427
  }
428
428
  const { id } = params;
@@ -439,7 +439,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
439
439
  wait: 'confirmed',
440
440
  readAt: claimed.snapshot.stamp,
441
441
  onStale: 'reject',
442
- claimRef: { id: claimed.lease.claimId },
442
+ claimRef: { id: claimed.lease.id },
443
443
  ...opts,
444
444
  }
445
445
  : {
@@ -454,7 +454,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
454
454
  }
455
455
  : {}),
456
456
  ...opts,
457
- ...(handle ? { claim: { id: handle.claimId } } : {}),
457
+ ...(handle ? { claim: { id: handle.id } } : {}),
458
458
  };
459
459
  // Local user update: `applyChanges` keeps change tracking ON so
460
460
  // the edited fields land in `modifiedProperties` and actually get
@@ -473,7 +473,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
473
473
  await operations.delete({ ...params, claim: handle });
474
474
  }
475
475
  finally {
476
- await handle.release();
476
+ await handle.release?.();
477
477
  }
478
478
  return;
479
479
  }
@@ -489,7 +489,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
489
489
  wait: 'confirmed',
490
490
  readAt: claimed.snapshot.stamp,
491
491
  onStale: 'reject',
492
- claimRef: { id: claimed.lease.claimId },
492
+ claimRef: { id: claimed.lease.id },
493
493
  ...opts,
494
494
  }
495
495
  : {
@@ -501,7 +501,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
501
501
  }
502
502
  : {}),
503
503
  ...opts,
504
- ...(handle ? { claim: { id: handle.claimId } } : {}),
504
+ ...(handle ? { claim: { id: handle.id } } : {}),
505
505
  };
506
506
  syncClient.delete(model, effective);
507
507
  await waitForMutation(model, effective);
@@ -6,6 +6,16 @@ export interface MintSessionContext {
6
6
  readonly apiKey: string;
7
7
  readonly baseUrl: string;
8
8
  readonly fetch?: typeof fetch;
9
+ /**
10
+ * Schema-key → wire typename map, supplied ONLY by the schema client
11
+ * (`Ablo`). A capability is scoped by the lowercased TYPENAME the Hub
12
+ * checks, but `can` is keyed by schema key — so `can: { documents: ['update'] }`
13
+ * on a model whose `typename` is overridden to `Document` must mint
14
+ * `document.update`, not `documents.update` (else the Hub denies it with
15
+ * `capability_scope_denied`). The schemaless `ApiClient` omits this map:
16
+ * there the `can` key already IS the wire token, so no translation applies.
17
+ */
18
+ readonly modelTypenames?: Readonly<Record<string, string>>;
9
19
  }
10
20
  /**
11
21
  * Mint a session token from an already-resolved `sk_` credential + base URL.