@abloatai/ablo 0.29.3 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +31 -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 +377 -84
  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
@@ -197,7 +197,7 @@ a model row. It's what `claim.state()` returns and what observers render.
197
197
  | `id` | `string` | The claim id (distinct from the target row id). |
198
198
  | `status` | `ClaimStatus` | `'active' \| 'queued' \| 'committed' \| 'expired' \| 'canceled'`. `active` = the holder; `queued` = waiting in line behind it. The other three are terminal states you only see on a claim you just finished — `committed` (released after a successful write), `expired` (TTL lapsed), `canceled` (released early). |
199
199
  | `target` | `EntityRef` | What is being coordinated (`{ model, id, field? }`). |
200
- | `reason` | `string` | Human-readable phase`'editing'`, `'writing'`, `'reviewing'`. Serialized on the wire as `action`. |
200
+ | `description` | `string` | Peer-visible description of the work the sentence another participant reads to decide whether to wait or move on (`'rewriting the risk section'`). Defaults to `'editing'`. |
201
201
  | `heldBy` | `string` | Participant holding (or waiting on) it (e.g. `'agent:forecaster'`). |
202
202
  | `participantKind` | `'user' \| 'agent' \| 'system'` | Who's behind it — a human (`user`), an AI (`agent`), or automated infrastructure (`system`). |
203
203
  | `position` | `number?` | 0-based place in the FIFO line — present only when `status: 'queued'` (`0` = next behind the holder). |
@@ -209,7 +209,7 @@ a model row. It's what `claim.state()` returns and what observers render.
209
209
  "id": "claim_8fJ2",
210
210
  "status": "active",
211
211
  "target": { "model": "weatherReports", "id": "report_stockholm" },
212
- "reason": "editing",
212
+ "description": "editing",
213
213
  "heldBy": "agent:forecaster",
214
214
  "participantKind": "agent",
215
215
  "createdAt": 1748160000000,
@@ -254,7 +254,7 @@ so two claimers can't both think they won.
254
254
  | name | type | required | description |
255
255
  |---|---|---|---|
256
256
  | `id` | `string` | yes | The row id — same id as `retrieve` / `update`. |
257
- | `options.reason` | `string` | no | Phase shown to observers (default `'editing'`). Serialized on the wire as `action`. |
257
+ | `options.description` | `string` | no | Peer-visible description of the work, shown to observers (default `'editing'`). |
258
258
  | `options.field` | `string` | no | Field-level target, for fine-grained claimed-state badges. |
259
259
  | `options.queue` | `boolean` | no | `true` (default) queues and waits for the lease. `false` is fail-fast — if another participant holds the row, reject immediately with `AbloClaimedError('entity_claimed')` instead of queuing (claim-or-skip, for work dedup where waiting would double-process). |
260
260
  | `options.maxQueueDepth` | `number` | no | Backpressure: reject with `AbloClaimedError('queue_too_deep')` instead of joining a line already `>= maxQueueDepth` deep. Omit to wait however deep the queue is. |
@@ -278,7 +278,17 @@ const weather = await weatherAgent.getWeather(report.location);
278
278
  await ablo.weatherReports.update({ id: report.id, data: { forecast: weather } });
279
279
  ```
280
280
 
281
- The claim releases when the `await using` scope exits — on return or throw.
281
+ The claim releases when the `await using` scope exits — **on return and on
282
+ throw.** The "on throw" is the whole reason to bind it with `await using`: if the
283
+ work between the claim and the write fails — the agent call errors, validation
284
+ rejects, you decide not to write — the scope unwinds, the lease is released, and
285
+ the next waiter is promoted, with no `finally` to remember. And nothing reaches
286
+ the server until `update`, so a failure *before* the write leaves the row exactly
287
+ as it was: claiming and committing are separate steps, so a failure between them
288
+ has nothing to roll back. (A failure *after* a successful write leaves that write
289
+ committed — pass an idempotency key on the write if you replay the block.) The
290
+ lower-level [`claim.release`](#claimrelease) shows the manual `try/finally`
291
+ equivalent for when you hold a claim without `await using`.
282
292
 
283
293
  ### Claim-gated reads
284
294
 
@@ -334,7 +344,7 @@ is free.
334
344
 
335
345
  ```ts
336
346
  const who = ablo.weatherReports.claim.state({ id: 'report_stockholm' });
337
- if (who) console.log(`${who.heldBy} is ${who.reason}`);
347
+ if (who) console.log(`${who.heldBy} is ${who.description}`);
338
348
  ```
339
349
 
340
350
  Returns the active claim state when the row is held, or `null` when it's free:
@@ -344,7 +354,7 @@ Returns the active claim state when the row is held, or `null` when it's free:
344
354
  "id": "claim_8fJ2",
345
355
  "status": "active",
346
356
  "target": { "model": "weatherReports", "id": "report_stockholm" },
347
- "reason": "editing",
357
+ "description": "editing",
348
358
  "heldBy": "agent:forecaster",
349
359
  "participantKind": "agent",
350
360
  "expiresAt": 1748160030000
@@ -405,7 +415,7 @@ Releasing **promotes the head of the queue**: the next waiter receives the claim
405
415
  **Example**
406
416
 
407
417
  ```ts
408
- const claim = await ablo.weatherReports.claim({ id: 'report_stockholm', reason: 'reviewing' });
418
+ const claim = await ablo.weatherReports.claim({ id: 'report_stockholm', description: 'reviewing' });
409
419
  const report = claim.data;
410
420
  try {
411
421
  const ok = await reviewExternally(report);
@@ -435,7 +445,7 @@ third of the TTL until release:
435
445
  ```ts
436
446
  await using claim = await ablo.reports.claim({
437
447
  id: 'report_q3',
438
- reason: 'generating',
448
+ description: 'generating',
439
449
  ttl: '5m',
440
450
  heartbeat: true, // or an explicit cadence: heartbeat: '2m'
441
451
  onHeartbeatLost: () => abortWork(),
@@ -475,6 +485,51 @@ A stateless worker holding **many** rows beats them all in one round trip:
475
485
  entry per extended lease. This is the socketless twin of the realtime
476
486
  keepalive, which already renews every held lease on each ping.
477
487
 
488
+ ### durability — what a claim survives
489
+
490
+ A lease belongs to your **identity** — the participant behind the credential —
491
+ not to the socket it was claimed on; the server keys each lease by participant
492
+ and `claimId`. That one fact decides what a claim lives through.
493
+
494
+ **A brief blip is transparent.** The realtime client reconnects on its own
495
+ (exponential backoff), and on each reconnect it re-announces every claim it
496
+ still holds, so the server renews those leases and peers never see them flicker.
497
+ A heartbeat that would land while the socket is momentarily down is skipped
498
+ rather than failed — the next tick retries once the connection is back. Nothing
499
+ to write: hold the claim and keep working.
500
+
501
+ **A crashed holder frees the claim quickly — and it is the keepalive, not the
502
+ TTL, that does it.** A dead holder is caught whichever way fires first: a clean
503
+ socket close releases immediately, and a silent socket that never sent a close
504
+ frame (a crashed tab, a dropped NAT) is reaped on the keepalive cycle (a ~30s
505
+ ping / 10s pong window) and released then. Either way the next waiter is
506
+ promoted within tens of seconds. This reclaim is **per-connection**, and it runs
507
+ whether or not the TTL is anywhere near lapsing. Release fires only when your
508
+ **last** connection goes, so a second connection under the same identity keeps
509
+ the claim held.
510
+
511
+ **The TTL is the deeper floor — for when the server itself restarts.** The live
512
+ claim roster is held in memory, so a server restart would lose it; the durable
513
+ lease in the coordination store carries the TTL, and a reconnecting client
514
+ re-announces its claims before that TTL lapses. So size `ttl` to cover a
515
+ deploy or restart window, not your work duration — beating covers the work
516
+ duration.
517
+
518
+ **To hold a claim across a holder crash, give it a durable identity.** A claim
519
+ that must outlive a single failure belongs to a process that stays up — a
520
+ backend worker or agent with its own credential — rather than one ephemeral
521
+ browser tab. On reconnect the SDK re-announces it; if the row was granted onward
522
+ while you were gone, that re-announce comes back as `AbloClaimedError`
523
+ (`claim_lost`) — re-claim (you rejoin the line fairly) and retry from the fresh
524
+ snapshot.
525
+
526
+ | the holder… | what happens to the claim |
527
+ | --- | --- |
528
+ | blips, then reconnects within the window | renewed automatically on reconnect — no interruption |
529
+ | crashes or drops for good | released within one keepalive cycle; the queue advances |
530
+ | still has a second live connection | survives — release fires only on the last connection |
531
+ | loses the server to a restart | rides the TTL in the coordination store; re-announced on reconnect |
532
+
478
533
  ### `watch` — presence for a set of rows
479
534
 
480
535
  Reading or claiming a row auto-enrolls you in its sync group, which is enough for
@@ -548,7 +603,7 @@ inspect the `code`.
548
603
 
549
604
  | error | `code` | thrown when | carries |
550
605
  |---|---|---|---|
551
- | `AbloClaimedError` | `claim_lost` | A held/queued claim was taken away (holder TTL lapse on disconnect, or revoke) while you were holding or waiting. | `claims?` |
606
+ | `AbloClaimedError` | `claim_lost` | A held/queued claim was taken away — the holder disconnected (reaped on the keepalive cycle), went silent past its TTL, or was revoked — while you were holding or waiting. | `claims?` |
552
607
  | `AbloClaimedError` | `claim_queued` | **HTTP transport only.** A contended `claim` (default `queue: true`) could not block-wait for the lease (no socket), so it rejected immediately instead of queueing. Retryable — re-attempt the claim. | `claims?` |
553
608
  | `AbloClaimedError` | `grant_timeout` | The optional `timeoutMs` elapsed while you were still queued for a grant. | `claims?` |
554
609
  | `AbloClaimedError` | `queue_too_deep` | `claim` was passed `maxQueueDepth` and the wait line was already that deep when you tried to join — fail-fast instead of waiting. | `claims?` |
@@ -205,12 +205,9 @@ rows.
205
205
  > connecting a new database. They are the seams that caused painful onboarding,
206
206
  > and `ablo connect` exists precisely to replace them.
207
207
 
208
- These older shapes connected Ablo to a database two other ways: by handing Ablo a
209
- **connection string** to operate directly (`databaseUrl` on the client, committing
210
- writes itself behind row-level security), or by exposing a **signed Data Source
208
+ This older shape connected Ablo to a database by exposing a **signed Data Source
211
209
  endpoint** built from an ORM adapter (`prismaDataSource` / `drizzleDataSource`,
212
210
  with `ablo_outbox` / `ablo_idempotency` bookkeeping and a reverse-channel
213
- connector for VPCs). Both required Ablo to either operate your database or proxy
214
- every write, and both have been superseded by logical replication, where Ablo only
215
- reads your WAL. If you are maintaining one of these integrations, migrate it to
216
- `ablo connect` at the next opportunity.
211
+ connector for VPCs). It required Ablo to proxy every write, and has been superseded
212
+ by logical replication, where Ablo only reads your WAL. If you are maintaining one
213
+ of these integrations, migrate it to `ablo connect` at the next opportunity.
@@ -48,14 +48,12 @@ Every schema model is backed by **your own database**. The SDK call shape is the
48
48
  same everywhere.
49
49
 
50
50
  In this guide — an app that already owns its backend and database — keep
51
- `DATABASE_URL` inside your app and expose a signed Data Source endpoint: Ablo
52
- coordinates each write and your app commits it to your Postgres. Do not pass
53
- `databaseUrl` to `Ablo(...)` here; application and agent code use `ABLO_API_KEY`.
54
-
55
- If instead you want Ablo to connect to your Postgres directly, pass `databaseUrl`
56
- (a live, server-only option) to `Ablo(...)`. That and the sandbox-only `apiKey`
57
- shape are the other two start states — [Connect Your Database](./data-sources.md)
58
- is the single source of truth for all three.
51
+ `DATABASE_URL` inside your app and connect it out of band: run `npx ablo connect`
52
+ for logical replication (Ablo tails your WAL), or expose a signed Data Source
53
+ endpoint where Ablo coordinates each write and your app commits it to your
54
+ Postgres. Either way, application and agent code hold only `ABLO_API_KEY` — the
55
+ client never sees a connection string. [Connect Your Database](./data-sources.md)
56
+ is the single source of truth for both paths.
59
57
 
60
58
  ## Test With Sandboxes
61
59
 
package/docs/migration.md CHANGED
@@ -234,9 +234,6 @@ The flat coordination methods are gone; everything lives under `claim`.
234
234
  + await ablo.task.claim.release(id)
235
235
  ```
236
236
 
237
- This release also added the `databaseUrl` option for the direct Postgres
238
- connector — additive, no migration required.
239
-
240
237
  ---
241
238
 
242
239
  ## 0.7.0 — legacy React hooks removed
@@ -215,11 +215,13 @@ of clobbering. Normal reads still work while the claim is held. If a server read
215
215
  should not return a row while someone else is mid-edit, pass `ifClaimed: 'fail'`
216
216
  to error out instead. Reads never block on a claim — to wait for a row to free
217
217
  up, `claim({ id })` it (the claim queues fairly behind the holder).
218
- Call `handle.release()` when your work is done.
218
+ Bind the handle with `await using` and the claim releases itself when the scope
219
+ exits — on success or on a throw, so a failing agent call never leaves the row
220
+ locked.
219
221
 
220
222
  ```ts
221
223
  // Claim the row so other participants serialize behind us while we work.
222
- const handle = await ablo.weatherReports.claim({
224
+ await using handle = await ablo.weatherReports.claim({
223
225
  id: 'weather_stockholm',
224
226
  reason: 'checking_weather',
225
227
  ttl: '2m',
@@ -236,15 +238,13 @@ await ablo.weatherReports.update({
236
238
  forecast: weather.summary,
237
239
  },
238
240
  });
239
-
240
- await handle.release();
241
+ // scope exit releases the claim — no manual release, even if the work threw
241
242
  ```
242
243
 
243
244
  Ablo does not fetch the weather. If another participant already holds the row,
244
245
  `claim` waits for them to finish, re-reads, and then hands you the fresh row.
245
246
  While you hold the claim, `update({ id, data })` rejects with `AbloStaleContextError`
246
- if someone else changed the row first — so you never overwrite work you didn't
247
- see. Call `handle.release()` once your work is done.
247
+ if someone else changed the row first — so you never overwrite work you didn't see.
248
248
 
249
249
  ## Multiplayer and claimed work
250
250
 
@@ -262,9 +262,8 @@ if (active) {
262
262
  console.log(`${active.heldBy} is ${active.reason}`);
263
263
  }
264
264
 
265
- const handle = await ablo.weatherReports.claim({ id: 'weather_stockholm' });
265
+ await using handle = await ablo.weatherReports.claim({ id: 'weather_stockholm' });
266
266
  await ablo.weatherReports.update({ id: handle.data.id, data: { status: 'ready' } });
267
- await handle.release();
268
267
  ```
269
268
 
270
269
  Use `{ queue: false }` on `claim` when work should be skipped instead of queued
@@ -104,14 +104,13 @@ the fresh row. Reads stay open; only acting on the row serializes.
104
104
 
105
105
  Every schema model is backed by your own database. There are three start states,
106
106
  all covered in [Connect Your Database](./data-sources.md) (the single source of
107
- truth): the sandbox (`apiKey` only, no database), a direct connection string
108
- (`databaseUrl` passed to `Ablo(...)`, a live, server-only option), or a signed
109
- Data Source endpoint where your app keeps the database credential and commits
110
- each write itself.
111
-
112
- If your database stays canonical behind a Data Source endpoint, do not pass
113
- `databaseUrl` to `Ablo(...)` trusted runtimes use `ABLO_API_KEY`. Browser code
114
- goes through `<AbloProvider>` or a scoped session route, never a raw API key.
107
+ truth): the sandbox (`apiKey` only, no database), logical replication set up with
108
+ `npx ablo connect` (Ablo tails your WAL), or a signed Data Source endpoint where
109
+ your app keeps the database credential and commits each write itself.
110
+
111
+ Your database connects out of band, so the client holds only `ABLO_API_KEY` —
112
+ never a connection string. Browser code goes through `<AbloProvider>` or a scoped
113
+ session route, never a raw API key.
115
114
 
116
115
  ## Rules of thumb
117
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.29.3",
3
+ "version": "0.30.1",
4
4
  "description": "The Collaboration Layer For AI Agents",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -136,9 +136,9 @@
136
136
  "prepack": "node scripts/strip-source-condition.mjs",
137
137
  "postpack": "node scripts/restore-source-condition.mjs",
138
138
  "pack:check": "node scripts/pack-check.mjs",
139
- "lint": "npm run lint:imports && npm run lint:errors && npm run lint:docs && npm run lint:mintlify",
140
- "build:docs": "node scripts/build-mintlify-docs.mjs",
141
- "lint:mintlify": "node scripts/build-mintlify-docs.mjs --check",
139
+ "lint": "npm run lint:imports && npm run lint:errors && npm run lint:docs && npm run lint:docs-site",
140
+ "build:docs": "node scripts/build-blume-docs.mjs",
141
+ "lint:docs-site": "node scripts/build-blume-docs.mjs --check",
142
142
  "lint:imports": "node scripts/check-js-extensions.mjs",
143
143
  "generate:errors": "tsx scripts/generate-error-docs.mts",
144
144
  "lint:errors": "tsx scripts/check-error-docs.mts",
@@ -1,19 +0,0 @@
1
- export interface RegisterDataSourceInput {
2
- /** The HTTP API base, for example `https://api.abloatai.com/api`. */
3
- readonly baseUrl: string;
4
- /** The secret key (`sk_…`) used to authenticate the call and derive the organization. */
5
- readonly apiKey: string | null;
6
- /** The Postgres connection string to register. */
7
- readonly databaseUrl: string;
8
- /** An optional Postgres schema; the server defaults to `public`. */
9
- readonly schema?: string;
10
- /** A custom fetch implementation for tests, proxies, or unusual runtimes. */
11
- readonly fetchImpl?: typeof fetch;
12
- }
13
- /**
14
- * Posts the connection string to the data-source registration route. Resolves once
15
- * the organization's data plane points at this database; otherwise throws an
16
- * {@link AbloError} with code `datasource_registration_failed`, so `ready()`
17
- * surfaces the failure instead of quietly bootstrapping against the wrong store.
18
- */
19
- export declare function registerDataSource(input: RegisterDataSourceInput): Promise<void>;
@@ -1,66 +0,0 @@
1
- /**
2
- * Registers a direct database connection as an organization's data source.
3
- *
4
- * When a client is constructed with `databaseUrl`, the SDK calls this before
5
- * bootstrap so the server points the organization's data plane at that connection.
6
- *
7
- * It posts to `POST /v1/datasources`, falling back to the older
8
- * `POST /v1/datasource` route on a 404 so registration still works against an
9
- * earlier server.
10
- *
11
- * The organization is derived on the server from the API key; the caller never
12
- * sends an organization id. The connection string is sent once over TLS and is
13
- * never echoed back — the server stores it as a secret and returns only a safe
14
- * projection of the data source (host, database, schema).
15
- */
16
- import { AbloError } from '../errors.js';
17
- /**
18
- * Posts the connection string to the data-source registration route. Resolves once
19
- * the organization's data plane points at this database; otherwise throws an
20
- * {@link AbloError} with code `datasource_registration_failed`, so `ready()`
21
- * surfaces the failure instead of quietly bootstrapping against the wrong store.
22
- */
23
- export async function registerDataSource(input) {
24
- if (!input.apiKey) {
25
- throw new AbloError('databaseUrl requires an apiKey to register the database connection (the org is derived from the key).', { code: 'datasource_registration_failed' });
26
- }
27
- const doFetch = input.fetchImpl ?? fetch;
28
- const base = input.baseUrl.replace(/\/+$/, '');
29
- const body = JSON.stringify({
30
- connectionString: input.databaseUrl,
31
- ...(input.schema ? { schema: input.schema } : {}),
32
- });
33
- const post = async (endpoint) => {
34
- try {
35
- return await doFetch(endpoint, {
36
- method: 'POST',
37
- headers: {
38
- 'content-type': 'application/json',
39
- authorization: `Bearer ${input.apiKey}`,
40
- },
41
- body,
42
- });
43
- }
44
- catch (cause) {
45
- throw new AbloError('Could not reach the Ablo API to register the database connection.', {
46
- code: 'datasource_registration_failed',
47
- cause,
48
- });
49
- }
50
- };
51
- let response = await post(`${base}/v1/datasources`);
52
- if (response.status === 404) {
53
- // The newer route is absent on an older server; use the earlier one.
54
- response = await post(`${base}/v1/datasource`);
55
- }
56
- if (!response.ok) {
57
- let detail = '';
58
- try {
59
- detail = (await response.text()).slice(0, 500);
60
- }
61
- catch {
62
- // ignore body read failures — the status alone is enough to fail loud
63
- }
64
- throw new AbloError(`Database connection registration failed (HTTP ${response.status}). ${detail}`, { code: 'datasource_registration_failed', httpStatus: response.status });
65
- }
66
- }