@abloatai/ablo 0.7.0 → 0.8.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 (83) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +54 -45
  3. package/dist/BaseSyncedStore.js +7 -3
  4. package/dist/SyncEngineContext.d.ts +2 -1
  5. package/dist/SyncEngineContext.js +5 -3
  6. package/dist/agent/session.js +3 -2
  7. package/dist/auth/index.js +39 -11
  8. package/dist/client/Ablo.d.ts +111 -3
  9. package/dist/client/Ablo.js +143 -10
  10. package/dist/client/ApiClient.d.ts +32 -0
  11. package/dist/client/ApiClient.js +76 -44
  12. package/dist/client/auth.d.ts +11 -1
  13. package/dist/client/auth.js +21 -2
  14. package/dist/client/createModelProxy.d.ts +107 -63
  15. package/dist/client/createModelProxy.js +65 -33
  16. package/dist/client/identity.js +14 -0
  17. package/dist/client/registerDataSource.d.ts +19 -0
  18. package/dist/client/registerDataSource.js +57 -0
  19. package/dist/client/validateAbloOptions.d.ts +2 -1
  20. package/dist/client/validateAbloOptions.js +8 -7
  21. package/dist/errorCodes.d.ts +23 -1
  22. package/dist/errorCodes.js +34 -1
  23. package/dist/errors.d.ts +52 -1
  24. package/dist/errors.js +140 -42
  25. package/dist/index.d.ts +9 -5
  26. package/dist/index.js +9 -5
  27. package/dist/keys/index.d.ts +61 -0
  28. package/dist/keys/index.js +151 -0
  29. package/dist/query/client.js +19 -8
  30. package/dist/react/AbloProvider.d.ts +25 -0
  31. package/dist/react/AbloProvider.js +97 -2
  32. package/dist/react/ClientSideSuspense.d.ts +1 -1
  33. package/dist/react/DefaultFallback.d.ts +1 -1
  34. package/dist/react/SyncGroupProvider.d.ts +1 -1
  35. package/dist/react/index.d.ts +3 -2
  36. package/dist/react/index.js +3 -2
  37. package/dist/react/useAblo.d.ts +4 -4
  38. package/dist/react/useAblo.js +10 -5
  39. package/dist/react/useReactive.js +16 -3
  40. package/dist/schema/serialize.d.ts +3 -3
  41. package/dist/schema/serialize.js +2 -2
  42. package/dist/sync/BootstrapHelper.js +46 -27
  43. package/dist/sync/ConnectionManager.d.ts +3 -1
  44. package/dist/sync/ConnectionManager.js +37 -1
  45. package/dist/sync/HydrationCoordinator.js +3 -2
  46. package/dist/sync/NetworkProbe.d.ts +8 -0
  47. package/dist/sync/NetworkProbe.js +24 -2
  48. package/dist/sync/SyncWebSocket.d.ts +1 -1
  49. package/dist/sync/SyncWebSocket.js +43 -53
  50. package/dist/sync/participants.js +5 -2
  51. package/dist/transactions/TransactionQueue.js +13 -1
  52. package/docs/api-keys.md +5 -5
  53. package/docs/api.md +101 -44
  54. package/docs/audit.md +16 -9
  55. package/docs/cli.md +27 -17
  56. package/docs/client-behavior.md +34 -20
  57. package/docs/coordination.md +40 -51
  58. package/docs/data-sources.md +21 -19
  59. package/docs/examples/agent-human.md +72 -28
  60. package/docs/examples/ai-sdk-tool.md +14 -11
  61. package/docs/examples/existing-python-backend.md +27 -16
  62. package/docs/examples/nextjs.md +21 -8
  63. package/docs/examples/scoped-agent.md +42 -27
  64. package/docs/examples/server-agent.md +27 -5
  65. package/docs/guarantees.md +26 -17
  66. package/docs/identity.md +65 -59
  67. package/docs/index.md +30 -19
  68. package/docs/integration-guide.md +52 -52
  69. package/docs/interaction-model.md +38 -26
  70. package/docs/mcp/claude-code.md +9 -17
  71. package/docs/mcp/cursor.md +6 -24
  72. package/docs/mcp/windsurf.md +6 -19
  73. package/docs/mcp.md +103 -26
  74. package/docs/quickstart.md +31 -39
  75. package/docs/react.md +15 -11
  76. package/docs/roadmap.md +13 -13
  77. package/docs/schema-contract.md +109 -0
  78. package/examples/README.md +8 -4
  79. package/examples/data-source/README.md +6 -2
  80. package/examples/data-source/run.ts +4 -3
  81. package/examples/quickstart.ts +1 -1
  82. package/llms.txt +27 -16
  83. package/package.json +6 -1
@@ -16,12 +16,13 @@
16
16
  * await sync.reports.delete(reportId);
17
17
  */
18
18
  import { z } from 'zod';
19
- import { AbloClaimedError, AbloError, AbloConnectionError, AbloValidationError, translateHttpError } from '../errors.js';
19
+ import { AbloClaimedError, AbloError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError, hasWireCode, toAbloError } from '../errors.js';
20
20
  import { LoadStrategy, PropertyType } from '../types/index.js';
21
21
  import { initSyncEngine } from '../context.js';
22
22
  import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../SyncEngineContext.js';
23
23
  import { alwaysOnline } from '../adapters/alwaysOnline.js';
24
24
  import { validateAbloOptions } from './validateAbloOptions.js';
25
+ import { exchangeApiKey } from '../auth/index.js';
25
26
  import { createInternalComponents } from './createInternalComponents.js';
26
27
  import { resolveParticipantIdentity } from './identity.js';
27
28
  import { Model } from '../Model.js';
@@ -32,7 +33,8 @@ import { awaitIntentGrant } from '../sync/awaitIntentGrant.js';
32
33
  import { createSnapshot } from '../sync/createSnapshot.js';
33
34
  import { createParticipantManager } from '../sync/participants.js';
34
35
  import { createProtocolClient, } from './ApiClient.js';
35
- import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveAuthToken, resolveBaseURL, } from './auth.js';
36
+ import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, } from './auth.js';
37
+ import { registerDataSource } from './registerDataSource.js';
36
38
  import { shouldUseInMemoryPersistence, } from './persistence.js';
37
39
  import { createModelProxy } from './createModelProxy.js';
38
40
  // ── Config derivation from schema ─────────────────────────────────────────
@@ -653,8 +655,10 @@ export function Ablo(options) {
653
655
  const authInput = { options, env };
654
656
  const configuredApiKey = resolveApiKey(authInput);
655
657
  const configuredAuthToken = resolveAuthToken(authInput);
658
+ const configuredDatabaseUrl = resolveDatabaseUrl(authInput);
656
659
  assertBrowserSafety({
657
660
  apiKey: configuredApiKey,
661
+ databaseUrl: configuredDatabaseUrl,
658
662
  dangerouslyAllowBrowser: options.dangerouslyAllowBrowser,
659
663
  });
660
664
  const { logger = consoleLogger } = internalOptions;
@@ -827,6 +831,19 @@ export function Ablo(options) {
827
831
  }
828
832
  _readyPromise = (async () => {
829
833
  try {
834
+ // Register the caller's own database for write-back BEFORE bootstrap, so
835
+ // the server resolves this org's data plane to the customer's DB rather
836
+ // than serving an empty/wrong store. The org is derived server-side from
837
+ // the API key. Idempotent server-side (register-or-update). Skipped when
838
+ // no `databaseUrl` was configured (Ablo-managed storage).
839
+ if (configuredDatabaseUrl) {
840
+ await registerDataSource({
841
+ baseUrl: resolveBootstrapBaseUrl({ url }),
842
+ apiKey: await resolveApiKeyValue(configuredApiKey),
843
+ databaseUrl: configuredDatabaseUrl,
844
+ ...(internalOptions.fetch ? { fetchImpl: internalOptions.fetch } : {}),
845
+ });
846
+ }
830
847
  // Resolve participant identity + scope. Three branches —
831
848
  // hosted-cloud apiKey exchange, self-derived from capability
832
849
  // token, or legacy explicit options. See `./identity.ts`.
@@ -836,7 +853,15 @@ export function Ablo(options) {
836
853
  url,
837
854
  kind,
838
855
  configuredApiKey,
839
- configuredAuthToken,
856
+ // Resolve identity against the LIVE token, not the construction-time
857
+ // `configuredAuthToken`. Consumers using `getToken` (apps/web) never
858
+ // pass `authToken` at construction — they call `setAuthToken()` before
859
+ // `ready()`, which updates `currentCapabilityToken`. Reading the frozen
860
+ // `configuredAuthToken` here made `/auth/identity` fire with no Bearer
861
+ // (→ `no_matching_provider` / `session_expired`) even though the JWT
862
+ // was present. Mirrors `authHeaders()`'s `currentCapabilityToken ??
863
+ // configuredAuthToken` precedence.
864
+ configuredAuthToken: currentCapabilityToken ?? configuredAuthToken,
840
865
  bootstrapHelper,
841
866
  logger,
842
867
  applyRotatedToken: (token) => {
@@ -902,7 +927,11 @@ export function Ablo(options) {
902
927
  }
903
928
  const result = current.value;
904
929
  if (!result.success) {
905
- throw result.error ?? new Error('Sync engine initialization failed');
930
+ throw result.error
931
+ ? toAbloError(result.error)
932
+ : new AbloConnectionError('Sync engine initialization failed', {
933
+ code: 'bootstrap_fetch_timeout',
934
+ });
906
935
  }
907
936
  // Wire presence + intents to the now-open transport.
908
937
  // `getSyncWebSocket()` returns non-null after a successful
@@ -916,11 +945,23 @@ export function Ablo(options) {
916
945
  logger.info('Sync engine ready', { models: Object.keys(schema.models).length });
917
946
  }
918
947
  catch (err) {
919
- const error = err instanceof Error ? err : new Error(String(err));
948
+ // Coerce so the rejection a consumer awaiting `ready()` catches is
949
+ // always an AbloError — connection setup is held to the same
950
+ // never-leak-untagged contract as the model operations.
951
+ const error = toAbloError(err);
920
952
  // Make sure syncStatus reflects the failure for observer() components
921
953
  store.syncStatus.state = 'error';
922
954
  store.syncStatus.error = error;
923
- logger.error('Sync engine failed to initialize', { error: error.message });
955
+ // Log the typed envelope (type + code + status), not just the bare
956
+ // message — so the console line names it as an Ablo error and carries
957
+ // the code (e.g. AbloAuthenticationError/identity_resolve_failed on a
958
+ // 401) instead of reading like an untagged failure.
959
+ logger.error('Sync engine failed to initialize', {
960
+ type: error.type,
961
+ code: error.code,
962
+ httpStatus: error.httpStatus,
963
+ error: error.message,
964
+ });
924
965
  throw error;
925
966
  }
926
967
  })();
@@ -1384,6 +1425,68 @@ export function Ablo(options) {
1384
1425
  ...modelProxies,
1385
1426
  ready,
1386
1427
  waitForFlush,
1428
+ setAuthToken(token) {
1429
+ // Same rotation path as the internal capability-token refresh
1430
+ // (`applyRotatedToken` in `ready()`): update the closure binding the
1431
+ // HTTP hydration provider reads, push to the bootstrap helper's header,
1432
+ // and swap it on the live WebSocket. Decoupled from `ready()` so a
1433
+ // refreshed JWT can be pushed at any point in the engine's lifetime.
1434
+ currentCapabilityToken = token;
1435
+ bootstrapHelper.setAuthToken(token);
1436
+ store.getSyncWebSocket()?.setCapabilityToken(token);
1437
+ },
1438
+ sessions: {
1439
+ // Stripe `ephemeralKeys.create` shape: a BACKEND (holding `sk_`) mints a
1440
+ // short-lived scoped token for one end user OR one agent. Thin wrapper over
1441
+ // the `/auth/capability` exchange, reshaped to a Stripe-style resource.
1442
+ async create(params) {
1443
+ const apiKey = await resolveApiKeyValue(configuredApiKey);
1444
+ if (!apiKey) {
1445
+ throw new AbloAuthenticationError('sessions.create requires a secret (sk_) API key — call it from your backend, not the browser.', { code: 'apikey_missing' });
1446
+ }
1447
+ const baseUrl = resolveBootstrapBaseUrl({
1448
+ url,
1449
+ bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
1450
+ });
1451
+ // Discriminate the union: `{ user }` → full-authority `ek_` (no op
1452
+ // allowlist); `{ agent, can }` → scoped `rk_`. `can: { Task: ['update'] }`
1453
+ // serializes to the wire allowlist `['task.update']` — the Hub matches
1454
+ // `${model.toLowerCase()}.${op}` (Hub.ts handleCommit).
1455
+ let participantKind;
1456
+ let participantId;
1457
+ let operations;
1458
+ if (params.user) {
1459
+ participantKind = 'user';
1460
+ participantId = params.user.id;
1461
+ operations = undefined;
1462
+ }
1463
+ else {
1464
+ participantKind = 'agent';
1465
+ participantId = params.agent.id;
1466
+ operations = Object.entries(params.can).flatMap(([model, ops]) => (ops ?? []).map((op) => `${model.toLowerCase()}.${op}`));
1467
+ }
1468
+ const res = await exchangeApiKey({
1469
+ apiKey,
1470
+ baseUrl,
1471
+ participantKind,
1472
+ participantId,
1473
+ ...(params.syncGroups ? { syncGroups: [...params.syncGroups] } : {}),
1474
+ ...(operations ? { operations } : {}),
1475
+ ttlSeconds: params.ttlSeconds ?? 900,
1476
+ ...(params.userMeta ? { userMeta: params.userMeta } : {}),
1477
+ ...(internalOptions.fetch ? { fetch: internalOptions.fetch } : {}),
1478
+ });
1479
+ return {
1480
+ object: 'session',
1481
+ id: res.capabilityId,
1482
+ token: res.token,
1483
+ expiresAt: res.expiresAt,
1484
+ organizationId: res.organizationId,
1485
+ scope: res.scope,
1486
+ userMeta: res.userMeta,
1487
+ };
1488
+ },
1489
+ },
1387
1490
  async dispose() {
1388
1491
  _refreshScheduler?.dispose();
1389
1492
  _refreshScheduler = null;
@@ -1491,8 +1594,24 @@ export function Ablo(options) {
1491
1594
  }),
1492
1595
  });
1493
1596
  if (!res.ok) {
1494
- const body = await res.text().catch(() => '<no body>');
1495
- throw new AbloError(`beginTurn failed: ${res.status} ${body}`, { code: 'turn_open_failed', httpStatus: res.status });
1597
+ const text = await res.text().catch(() => '');
1598
+ let parsed = text;
1599
+ if (text) {
1600
+ try {
1601
+ parsed = JSON.parse(text);
1602
+ }
1603
+ catch {
1604
+ /* keep raw text */
1605
+ }
1606
+ }
1607
+ // Preserve the server's structured envelope (code/message/doc_url) when
1608
+ // present; fall back to turn_open_failed for a bare/non-Ablo body.
1609
+ throw hasWireCode(parsed)
1610
+ ? translateHttpError(res.status, parsed, res.headers.get('x-request-id') ?? undefined)
1611
+ : new AbloError(`beginTurn failed: ${res.status} ${text}`, {
1612
+ code: 'turn_open_failed',
1613
+ httpStatus: res.status,
1614
+ });
1496
1615
  }
1497
1616
  const json = (await res.json());
1498
1617
  const turnId = json.turnId;
@@ -1515,8 +1634,22 @@ export function Ablo(options) {
1515
1634
  }),
1516
1635
  });
1517
1636
  if (!closeRes.ok) {
1518
- const body = await closeRes.text().catch(() => '<no body>');
1519
- throw new AbloError(`closeTurn failed: ${closeRes.status} ${body}`, { code: 'turn_close_failed', httpStatus: closeRes.status });
1637
+ const text = await closeRes.text().catch(() => '');
1638
+ let parsed = text;
1639
+ if (text) {
1640
+ try {
1641
+ parsed = JSON.parse(text);
1642
+ }
1643
+ catch {
1644
+ /* keep raw text */
1645
+ }
1646
+ }
1647
+ throw hasWireCode(parsed)
1648
+ ? translateHttpError(closeRes.status, parsed, closeRes.headers.get('x-request-id') ?? undefined)
1649
+ : new AbloError(`closeTurn failed: ${closeRes.status} ${text}`, {
1650
+ code: 'turn_close_failed',
1651
+ httpStatus: closeRes.status,
1652
+ });
1520
1653
  }
1521
1654
  };
1522
1655
  const dispose = () => {
@@ -73,10 +73,42 @@ export interface CapabilityRevocation {
73
73
  readonly deleted: boolean;
74
74
  readonly activeSessionsClosed?: number;
75
75
  }
76
+ export interface CapabilityRotateOptions {
77
+ /**
78
+ * Overlap window — the OLD token keeps authenticating for this long after
79
+ * rotation, so you can deploy the replacement with zero downtime. Default
80
+ * 24h server-side.
81
+ */
82
+ readonly grace?: Duration;
83
+ readonly graceSeconds?: number;
84
+ /**
85
+ * Lifetime of the REPLACEMENT capability. Omit to inherit the original's
86
+ * lifetime.
87
+ */
88
+ readonly lease?: Duration;
89
+ readonly leaseSeconds?: number;
90
+ }
91
+ /** The fresh capability returned by `rotate`, plus a pointer to the old one. */
92
+ export interface RotatedCapability extends Capability {
93
+ /**
94
+ * The capability that was rotated out. Its token keeps working until
95
+ * `expiresAt` (the end of the grace window), then expires.
96
+ */
97
+ readonly rotatedFrom: {
98
+ readonly id: string;
99
+ readonly expiresAt: string;
100
+ };
101
+ }
76
102
  export interface CapabilityResource {
77
103
  create(options: CapabilityCreateOptions): Promise<Capability>;
78
104
  retrieve(id: string): Promise<CapabilityRecord>;
79
105
  revoke(id: string): Promise<CapabilityRevocation>;
106
+ /**
107
+ * Rotate with overlap (Stripe's "roll" model): mint a fresh capability
108
+ * carrying the SAME scope, and keep the old token working for a grace
109
+ * window so you can roll out the replacement without downtime.
110
+ */
111
+ rotate(id: string, options?: CapabilityRotateOptions): Promise<RotatedCapability>;
80
112
  /**
81
113
  * Alias for `create`. Kept because "mint" is common capability-token
82
114
  * language, but `create` is the canonical SDK verb.
@@ -439,6 +439,35 @@ export function createProtocolClient(options) {
439
439
  activeSessionsClosed: body.activeSessionsClosed,
440
440
  };
441
441
  },
442
+ async rotate(id, rotateOptions = {}) {
443
+ const graceSeconds = rotateOptions.graceSeconds ??
444
+ (rotateOptions.grace !== undefined ? toSeconds(rotateOptions.grace) : undefined);
445
+ const leaseSeconds = rotateOptions.leaseSeconds ??
446
+ (rotateOptions.lease !== undefined ? toSeconds(rotateOptions.lease) : undefined);
447
+ const body = await requestJson(`/v1/capabilities/${encodeURIComponent(id)}/rotate`, {
448
+ method: 'POST',
449
+ body: JSON.stringify({
450
+ ...(graceSeconds !== undefined ? { graceSeconds } : {}),
451
+ ...(leaseSeconds !== undefined ? { ttlSeconds: leaseSeconds } : {}),
452
+ }),
453
+ });
454
+ const newId = body.capabilityId ?? body.id;
455
+ if (!newId) {
456
+ throw new AbloValidationError('Capability rotate response did not include an id.', { code: 'capability_id_missing' });
457
+ }
458
+ return {
459
+ id: newId,
460
+ token: body.token,
461
+ expiresAt: body.expiresAt,
462
+ organizationId: body.organizationId,
463
+ scope: body.scope,
464
+ rotatedFrom: {
465
+ id: body.rotatedFrom.capabilityId ?? body.rotatedFrom.id ?? id,
466
+ expiresAt: body.rotatedFrom.expiresAt,
467
+ },
468
+ client: () => childClient(body.token),
469
+ };
470
+ },
442
471
  mint(options) {
443
472
  return capabilities.create(options);
444
473
  },
@@ -548,6 +577,50 @@ export function createProtocolClient(options) {
548
577
  claims: query.claims ?? [],
549
578
  };
550
579
  }
580
+ /**
581
+ * Single-op mutation over the model-scoped routes — the canonical surface
582
+ * that mirrors `ablo.<model>.create/update/delete`:
583
+ *
584
+ * POST /v1/models/:model create
585
+ * PATCH /v1/models/:model/:id update
586
+ * DELETE /v1/models/:model/:id delete
587
+ *
588
+ * This replaces the previous indirection through `POST /v1/commits`. The raw
589
+ * `commits.create(...)` resource is still the path for ATOMIC MULTI-OP
590
+ * envelopes — this helper is the one-op, one-record path only.
591
+ */
592
+ async function mutateModel(action, modelName, id, data, options) {
593
+ const clientTxId = createClientTxId(options?.idempotencyKey);
594
+ const encModel = encodeURIComponent(modelName);
595
+ const path = action === 'create'
596
+ ? `/v1/models/${encModel}`
597
+ : `/v1/models/${encModel}/${encodeURIComponent(id)}`;
598
+ const method = action === 'create' ? 'POST' : action === 'update' ? 'PATCH' : 'DELETE';
599
+ const requestBody = {
600
+ idempotencyKey: clientTxId,
601
+ intent: normalizeIntentId(options?.intent),
602
+ onStale: options?.onStale,
603
+ readAt: options?.readAt,
604
+ };
605
+ if (action === 'create')
606
+ requestBody.id = id;
607
+ if (data !== undefined)
608
+ requestBody.data = data;
609
+ const body = await requestJson(path, {
610
+ method,
611
+ idempotencyKey: clientTxId,
612
+ body: JSON.stringify(requestBody),
613
+ });
614
+ // `requestJson` throws via `translateHttpError` on any non-2xx, so reaching
615
+ // here implies success. Narrow `status` to the `CommitWait`-compatible
616
+ // subset; `'rejected'` only appears on a thrown rejection body.
617
+ const status = body.status === 'queued' ? 'queued' : 'confirmed';
618
+ return {
619
+ id: body.serverTxId ?? body.id ?? body.clientTxId ?? clientTxId,
620
+ status,
621
+ lastSyncId: body.lastSyncId,
622
+ };
623
+ }
551
624
  function model(name) {
552
625
  return {
553
626
  retrieve(id, options) {
@@ -556,56 +629,15 @@ export function createProtocolClient(options) {
556
629
  async create(data, mutationOptions) {
557
630
  const id = mutationOptions?.id ?? createModelId();
558
631
  await applyClaimedPolicy({ model: name, id }, mutationOptions);
559
- return commits.create({
560
- intent: mutationOptions?.intent,
561
- idempotencyKey: mutationOptions?.idempotencyKey,
562
- readAt: mutationOptions?.readAt,
563
- onStale: mutationOptions?.onStale,
564
- wait: mutationOptions?.wait,
565
- operations: [
566
- {
567
- action: 'create',
568
- model: name,
569
- id,
570
- data,
571
- },
572
- ],
573
- });
632
+ return mutateModel('create', name, id, data, mutationOptions);
574
633
  },
575
634
  async update(id, data, mutationOptions) {
576
635
  await applyClaimedPolicy({ model: name, id }, mutationOptions);
577
- return commits.create({
578
- intent: mutationOptions?.intent,
579
- idempotencyKey: mutationOptions?.idempotencyKey,
580
- readAt: mutationOptions?.readAt,
581
- onStale: mutationOptions?.onStale,
582
- wait: mutationOptions?.wait,
583
- operations: [
584
- {
585
- action: 'update',
586
- model: name,
587
- id,
588
- data,
589
- },
590
- ],
591
- });
636
+ return mutateModel('update', name, id, data, mutationOptions);
592
637
  },
593
638
  async delete(id, mutationOptions) {
594
639
  await applyClaimedPolicy({ model: name, id }, mutationOptions);
595
- return commits.create({
596
- intent: mutationOptions?.intent,
597
- idempotencyKey: mutationOptions?.idempotencyKey,
598
- readAt: mutationOptions?.readAt,
599
- onStale: mutationOptions?.onStale,
600
- wait: mutationOptions?.wait,
601
- operations: [
602
- {
603
- action: 'delete',
604
- model: name,
605
- id,
606
- },
607
- ],
608
- });
640
+ return mutateModel('delete', name, id, undefined, mutationOptions);
609
641
  },
610
642
  };
611
643
  }
@@ -29,6 +29,7 @@ export interface AuthResolveInput {
29
29
  readonly apiKey?: string | ApiKeySetter | null;
30
30
  readonly authToken?: string | null;
31
31
  readonly baseURL?: string | null;
32
+ readonly databaseUrl?: string | null;
32
33
  readonly dangerouslyAllowBrowser?: boolean;
33
34
  };
34
35
  readonly env: Record<string, string | undefined>;
@@ -41,16 +42,25 @@ export interface AuthResolveInput {
41
42
  export declare function readProcessEnv(): Record<string, string | undefined>;
42
43
  export declare function resolveApiKey(input: AuthResolveInput): string | ApiKeySetter | null;
43
44
  export declare function resolveAuthToken(input: AuthResolveInput): string | null;
45
+ /**
46
+ * Resolve the customer's own-Postgres connection string for write-back
47
+ * (dedicated/BYO tenant). Falls back to `DATABASE_URL` — the Prisma-style
48
+ * convention — so a server-side app that already exports it needs no extra
49
+ * config. Returns null for Ablo-managed storage (the hosted default).
50
+ */
51
+ export declare function resolveDatabaseUrl(input: AuthResolveInput): string | null;
44
52
  export declare const ABLO_DEFAULT_BASE_URL = "wss://mesh.ablo.finance";
45
53
  export declare function resolveBaseURL(input: AuthResolveInput): string;
46
54
  /**
47
55
  * Browser guard — apiKey is server-side-only by default. Same check
48
56
  * Anthropic, OpenAI, and Stripe ship: shipping `sk_live_...` to a
49
57
  * browser exposes it in every visitor's network tab. Consumers opt
50
- * in explicitly when they have a publishable key or a server proxy.
58
+ * in explicitly when the browser holds a minted session token
59
+ * (`ek_`/`rk_`) or routes through a server proxy.
51
60
  */
52
61
  export declare function assertBrowserSafety(input: {
53
62
  apiKey: string | ApiKeySetter | null;
63
+ databaseUrl?: string | null;
54
64
  dangerouslyAllowBrowser: boolean | undefined;
55
65
  }): void;
56
66
  /**
@@ -27,6 +27,15 @@ export function resolveApiKey(input) {
27
27
  export function resolveAuthToken(input) {
28
28
  return input.options.authToken ?? null;
29
29
  }
30
+ /**
31
+ * Resolve the customer's own-Postgres connection string for write-back
32
+ * (dedicated/BYO tenant). Falls back to `DATABASE_URL` — the Prisma-style
33
+ * convention — so a server-side app that already exports it needs no extra
34
+ * config. Returns null for Ablo-managed storage (the hosted default).
35
+ */
36
+ export function resolveDatabaseUrl(input) {
37
+ return input.options.databaseUrl ?? input.env.DATABASE_URL ?? null;
38
+ }
30
39
  export const ABLO_DEFAULT_BASE_URL = 'wss://mesh.ablo.finance';
31
40
  export function resolveBaseURL(input) {
32
41
  return input.options.baseURL ?? ABLO_DEFAULT_BASE_URL;
@@ -35,11 +44,13 @@ export function resolveBaseURL(input) {
35
44
  * Browser guard — apiKey is server-side-only by default. Same check
36
45
  * Anthropic, OpenAI, and Stripe ship: shipping `sk_live_...` to a
37
46
  * browser exposes it in every visitor's network tab. Consumers opt
38
- * in explicitly when they have a publishable key or a server proxy.
47
+ * in explicitly when the browser holds a minted session token
48
+ * (`ek_`/`rk_`) or routes through a server proxy.
39
49
  */
40
50
  export function assertBrowserSafety(input) {
51
+ const inBrowser = typeof window !== 'undefined';
41
52
  if (!input.dangerouslyAllowBrowser &&
42
- typeof window !== 'undefined' &&
53
+ inBrowser &&
43
54
  typeof input.apiKey === 'string' &&
44
55
  input.apiKey.startsWith('sk_')) {
45
56
  throw new AbloAuthenticationError("It looks like you're running in a browser-like environment.\n\n" +
@@ -49,6 +60,14 @@ export function assertBrowserSafety(input) {
49
60
  '`dangerouslyAllowBrowser` option to `true`, e.g.,\n\n' +
50
61
  ' Ablo({ schema, apiKey, dangerouslyAllowBrowser: true });\n', { code: 'browser_apikey_blocked' });
51
62
  }
63
+ // `databaseUrl` carries DB credentials and is NEVER browser-safe, so
64
+ // `dangerouslyAllowBrowser` does not override it. Register your database from
65
+ // a server-side runtime.
66
+ if (inBrowser && typeof input.databaseUrl === 'string' && input.databaseUrl.length > 0) {
67
+ throw new AbloAuthenticationError('Ablo `databaseUrl` cannot be used in a browser-like environment — it ' +
68
+ 'carries your database credentials. Initialize the client with ' +
69
+ '`databaseUrl` from a server-side runtime only.', { code: 'browser_database_url_blocked' });
70
+ }
52
71
  }
53
72
  /**
54
73
  * Resolve an `ApiKeySetter` callable to its current string value.