@abloatai/ablo 0.8.0 → 0.9.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.
- package/CHANGELOG.md +40 -1
- package/README.md +32 -27
- package/dist/BaseSyncedStore.d.ts +73 -0
- package/dist/BaseSyncedStore.js +172 -2
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/agent/session.js +3 -3
- package/dist/ai-sdk/coordination-context.js +4 -0
- package/dist/ai-sdk/index.d.ts +56 -47
- package/dist/ai-sdk/index.js +56 -47
- package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
- package/dist/ai-sdk/intent-broadcast.js +11 -4
- package/dist/ai-sdk/wrap.d.ts +14 -11
- package/dist/ai-sdk/wrap.js +11 -13
- package/dist/auth/credentialSource.d.ts +34 -0
- package/dist/auth/credentialSource.js +63 -0
- package/dist/auth/index.d.ts +2 -22
- package/dist/auth/index.js +4 -42
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +160 -42
- package/dist/client/Ablo.js +145 -75
- package/dist/client/ApiClient.d.ts +20 -4
- package/dist/client/ApiClient.js +166 -28
- package/dist/client/auth.d.ts +14 -5
- package/dist/client/auth.js +60 -7
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +130 -66
- package/dist/client/createModelProxy.js +152 -49
- package/dist/client/httpClient.d.ts +71 -0
- package/dist/client/httpClient.js +69 -0
- package/dist/client/identity.d.ts +2 -6
- package/dist/client/identity.js +49 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +3 -3
- package/dist/client/registerDataSource.js +11 -9
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/core/DatabaseManager.js +30 -2
- package/dist/core/openIDBWithTimeout.d.ts +36 -0
- package/dist/core/openIDBWithTimeout.js +88 -1
- package/dist/errorCodes.d.ts +70 -1
- package/dist/errorCodes.js +108 -9
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +72 -22
- package/dist/index.d.ts +17 -8
- package/dist/index.js +15 -6
- package/dist/keys/index.d.ts +16 -1
- package/dist/keys/index.js +26 -6
- package/dist/mutators/UndoManager.d.ts +86 -50
- package/dist/mutators/UndoManager.js +129 -22
- package/dist/mutators/inverseOp.d.ts +129 -0
- package/dist/mutators/inverseOp.js +74 -0
- package/dist/mutators/readerActions.d.ts +1 -1
- package/dist/mutators/undoApply.d.ts +42 -0
- package/dist/mutators/undoApply.js +143 -0
- package/dist/query/client.d.ts +10 -9
- package/dist/query/client.js +3 -6
- package/dist/react/AbloProvider.d.ts +23 -126
- package/dist/react/AbloProvider.js +62 -199
- package/dist/react/useAblo.d.ts +2 -2
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/schema/ddl.d.ts +26 -3
- package/dist/schema/ddl.js +152 -4
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.js +12 -0
- package/dist/schema/model.d.ts +11 -0
- package/dist/schema/model.js +2 -0
- package/dist/schema/openapi.d.ts +28 -0
- package/dist/schema/openapi.js +118 -0
- package/dist/schema/plane.d.ts +23 -0
- package/dist/schema/plane.js +19 -0
- package/dist/schema/relation.d.ts +20 -0
- package/dist/schema/serialize.d.ts +4 -0
- package/dist/schema/serialize.js +4 -0
- package/dist/schema/sync-delta-row.d.ts +157 -0
- package/dist/schema/sync-delta-row.js +102 -0
- package/dist/schema/sync-delta-wire.d.ts +180 -0
- package/dist/schema/sync-delta-wire.js +102 -0
- package/dist/server/adapter.d.ts +156 -0
- package/dist/server/adapter.js +19 -0
- package/dist/server/commit.d.ts +82 -0
- package/dist/server/commit.js +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +1 -0
- package/dist/server/next.d.ts +51 -0
- package/dist/server/next.js +47 -0
- package/dist/server/read-config.d.ts +60 -0
- package/dist/server/read-config.js +8 -0
- package/dist/server/storage-mode.d.ts +17 -0
- package/dist/server/storage-mode.js +12 -0
- package/dist/source/adapter.d.ts +59 -0
- package/dist/source/adapter.js +19 -0
- package/dist/source/adapters/drizzle.d.ts +34 -0
- package/dist/source/adapters/drizzle.js +147 -0
- package/dist/source/adapters/memory.d.ts +12 -0
- package/dist/source/adapters/memory.js +114 -0
- package/dist/source/adapters/prisma.d.ts +57 -0
- package/dist/source/adapters/prisma.js +199 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +143 -0
- package/dist/source/contract.js +98 -0
- package/dist/source/index.d.ts +61 -10
- package/dist/source/index.js +98 -0
- package/dist/source/next.d.ts +33 -0
- package/dist/source/next.js +26 -0
- package/dist/sync/BootstrapHelper.d.ts +10 -0
- package/dist/sync/BootstrapHelper.js +10 -15
- package/dist/sync/ConnectionManager.d.ts +55 -1
- package/dist/sync/ConnectionManager.js +155 -16
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +238 -39
- package/dist/sync/NetworkProbe.d.ts +58 -24
- package/dist/sync/NetworkProbe.js +118 -42
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +70 -36
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/types/streams.d.ts +9 -0
- package/dist/utils/mobx-setup.js +1 -0
- package/dist/webhooks/events.d.ts +38 -0
- package/dist/webhooks/events.js +40 -0
- package/dist/webhooks/index.d.ts +10 -0
- package/dist/webhooks/index.js +10 -0
- package/dist/wire/errorEnvelope.d.ts +34 -0
- package/dist/wire/errorEnvelope.js +86 -0
- package/dist/wire/frames.d.ts +119 -0
- package/dist/wire/frames.js +1 -0
- package/dist/wire/index.d.ts +24 -0
- package/dist/wire/index.js +21 -0
- package/dist/wire/listEnvelope.d.ts +45 -0
- package/dist/wire/listEnvelope.js +17 -0
- package/docs/api.md +47 -44
- package/docs/cli.md +44 -44
- package/docs/client-behavior.md +30 -30
- package/docs/coordination.md +33 -36
- package/docs/data-sources.md +35 -15
- package/docs/examples/agent-human.md +45 -43
- package/docs/examples/ai-sdk-tool.md +20 -16
- package/docs/examples/existing-python-backend.md +16 -12
- package/docs/examples/nextjs.md +14 -12
- package/docs/examples/scoped-agent.md +1 -1
- package/docs/examples/server-agent.md +24 -21
- package/docs/guarantees.md +15 -13
- package/docs/index.md +1 -1
- package/docs/integration-guide.md +30 -30
- package/docs/interaction-model.md +19 -23
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +2 -2
- package/docs/mcp.md +6 -6
- package/docs/quickstart.md +41 -31
- package/docs/react.md +13 -9
- package/docs/schema-contract.md +12 -10
- package/docs/the-loop.md +21 -0
- package/examples/data-source/README.md +4 -5
- package/examples/data-source/customer-server.ts +27 -25
- package/llms.txt +28 -5
- package/package.json +43 -3
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* exposes the async server reads `retrieve` / `list`, the synchronous
|
|
11
11
|
* local-graph snapshots `get` / `getAll` / `getCount`, the writes
|
|
12
12
|
* `create` / `update` / `delete`, the coordination namespace `claim`
|
|
13
|
-
* (`claim(id
|
|
13
|
+
* (`claim({ id })` plus `claim.state` / `claim.queue` / `claim.release` /
|
|
14
14
|
* `claim.reorder`), and `onChange`. The factory returns a plain object; the
|
|
15
15
|
* client assembles the `ablo.<model>` lookup table from these.
|
|
16
16
|
*/
|
|
@@ -61,9 +61,23 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
61
61
|
await syncClient.waitForConfirmation(model.getModelName(), model.id);
|
|
62
62
|
};
|
|
63
63
|
// Claims this proxy currently holds, keyed by entity id. Lets the flat
|
|
64
|
-
// `release(id)` and `update(id)` find the lease + snapshot a `claim(id)`
|
|
64
|
+
// `release({ id })` and `update({ id, data })` find the lease + snapshot a `claim({ id })`
|
|
65
65
|
// took — no per-call handle. Released on dispose, explicit release, or TTL.
|
|
66
66
|
const activeClaims = new Map();
|
|
67
|
+
const isClaimHandle = (value) => typeof value === 'object' &&
|
|
68
|
+
value !== null &&
|
|
69
|
+
value.object === 'claim' &&
|
|
70
|
+
typeof value.claimId === 'string' &&
|
|
71
|
+
typeof value.release === 'function';
|
|
72
|
+
const claimMeta = (options) => {
|
|
73
|
+
if (!options?.description)
|
|
74
|
+
return options?.meta;
|
|
75
|
+
return { ...(options.meta ?? {}), description: options.description };
|
|
76
|
+
};
|
|
77
|
+
const mutationOptions = (params) => {
|
|
78
|
+
const { id: _id, data: _data, claim: _claim, ...rest } = params;
|
|
79
|
+
return rest;
|
|
80
|
+
};
|
|
67
81
|
const releaseClaim = async (id) => {
|
|
68
82
|
const held = activeClaims.get(id);
|
|
69
83
|
if (!held)
|
|
@@ -71,10 +85,11 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
71
85
|
activeClaims.delete(id);
|
|
72
86
|
await held.lease.release();
|
|
73
87
|
};
|
|
74
|
-
const takeClaim = async (
|
|
88
|
+
const takeClaim = async (params) => {
|
|
75
89
|
if (!collaboration) {
|
|
76
90
|
throw new AbloValidationError(`Model "${schemaKey}" cannot claim a row without collaboration wiring.`, { code: 'model_claim_not_configured' });
|
|
77
91
|
}
|
|
92
|
+
const { id, ...options } = params;
|
|
78
93
|
// Is someone ELSE already on this target? Read the local coordination
|
|
79
94
|
// snapshot up front — it decides whether we'll need to re-read after the
|
|
80
95
|
// claim (a free / already-mine target can't have changed under us).
|
|
@@ -109,6 +124,9 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
109
124
|
model: schemaKey,
|
|
110
125
|
id,
|
|
111
126
|
...(options?.field ? { field: options.field } : {}),
|
|
127
|
+
...(options?.path ? { path: options.path } : {}),
|
|
128
|
+
...(options?.range ? { range: options.range } : {}),
|
|
129
|
+
...(claimMeta(options) ? { meta: claimMeta(options) } : {}),
|
|
112
130
|
},
|
|
113
131
|
action: options?.action ?? 'editing',
|
|
114
132
|
ttl: options?.ttl,
|
|
@@ -124,53 +142,54 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
124
142
|
}
|
|
125
143
|
const snapshot = collaboration.createSnapshot(schemaKey, id);
|
|
126
144
|
activeClaims.set(id, { lease, snapshot });
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
const target = {
|
|
146
|
+
model: schemaKey,
|
|
147
|
+
id,
|
|
148
|
+
...(options?.field ? { field: options.field } : {}),
|
|
149
|
+
...(options?.path ? { path: options.path } : {}),
|
|
150
|
+
...(options?.range ? { range: options.range } : {}),
|
|
151
|
+
...(claimMeta(options) ? { meta: claimMeta(options) } : {}),
|
|
152
|
+
};
|
|
153
|
+
const release = () => releaseClaim(id);
|
|
154
|
+
return {
|
|
155
|
+
object: 'claim',
|
|
156
|
+
claimId: lease.id,
|
|
157
|
+
target,
|
|
158
|
+
action: options?.action ?? 'editing',
|
|
159
|
+
...(options?.description ? { description: options.description } : {}),
|
|
160
|
+
data: modelAsRow(model),
|
|
161
|
+
release,
|
|
162
|
+
revoke: () => {
|
|
163
|
+
void release();
|
|
164
|
+
},
|
|
165
|
+
[Symbol.asyncDispose]: release,
|
|
166
|
+
};
|
|
135
167
|
};
|
|
136
|
-
|
|
137
|
-
if (typeof a === 'function') {
|
|
138
|
-
return (async () => {
|
|
139
|
-
const row = await takeClaim(id, b);
|
|
140
|
-
try {
|
|
141
|
-
return await a(row);
|
|
142
|
-
}
|
|
143
|
-
finally {
|
|
144
|
-
await releaseClaim(id);
|
|
145
|
-
}
|
|
146
|
-
})();
|
|
147
|
-
}
|
|
148
|
-
return takeClaim(id, a);
|
|
149
|
-
}
|
|
168
|
+
const claim = (params) => takeClaim(params);
|
|
150
169
|
// `claim` is a callable namespace: invoke it to take a claim, reach its
|
|
151
170
|
// members to read/steer the coordination plane. Attach the readers to the
|
|
152
171
|
// guarded callable so `ablo.<model>.claim(...)` and `ablo.<model>.claim.state(...)`
|
|
153
172
|
// are the same object.
|
|
154
173
|
const claimApi = Object.assign(guard(claim), {
|
|
155
|
-
state(
|
|
156
|
-
return collaboration?.observe({ model: schemaKey, id }) ?? null;
|
|
174
|
+
state(params) {
|
|
175
|
+
return collaboration?.observe({ model: schemaKey, id: params.id }) ?? null;
|
|
157
176
|
},
|
|
158
|
-
queue(
|
|
177
|
+
queue(params) {
|
|
159
178
|
return {
|
|
160
179
|
object: 'list',
|
|
161
|
-
data: collaboration?.queue({ model: schemaKey, id }) ?? [],
|
|
180
|
+
data: collaboration?.queue({ model: schemaKey, id: params.id }) ?? [],
|
|
162
181
|
};
|
|
163
182
|
},
|
|
164
|
-
reorder(
|
|
165
|
-
collaboration?.reorder({ model: schemaKey, id }, order);
|
|
183
|
+
reorder(params) {
|
|
184
|
+
collaboration?.reorder({ model: schemaKey, id: params.id }, params.order);
|
|
166
185
|
},
|
|
167
|
-
release: guard((
|
|
186
|
+
release: guard((params) => releaseClaim(isClaimHandle(params) ? params.target.id : params.id)),
|
|
168
187
|
});
|
|
169
188
|
const operations = {
|
|
170
|
-
retrieve: guard(async (
|
|
189
|
+
retrieve: guard(async (params) => {
|
|
171
190
|
const rows = await load({
|
|
172
|
-
...
|
|
173
|
-
where: { id },
|
|
191
|
+
...params,
|
|
192
|
+
where: { id: params.id },
|
|
174
193
|
limit: 1,
|
|
175
194
|
});
|
|
176
195
|
return rows[0];
|
|
@@ -215,46 +234,130 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
215
234
|
getCount(options) {
|
|
216
235
|
return this.getAll(options).length;
|
|
217
236
|
},
|
|
218
|
-
create: guard(async (
|
|
219
|
-
// TODO(options-persistence): stash `
|
|
237
|
+
create: guard(async (params) => {
|
|
238
|
+
// TODO(options-persistence): stash `params` alongside the
|
|
220
239
|
// queued transaction so idempotencyKey survives offline flush.
|
|
240
|
+
const id = params.id ?? Model.generateId();
|
|
241
|
+
const opts = mutationOptions(params);
|
|
242
|
+
const claim = params.claim;
|
|
243
|
+
let autoLease;
|
|
244
|
+
if (claim && !isClaimHandle(claim)) {
|
|
245
|
+
if (!collaboration) {
|
|
246
|
+
throw new AbloValidationError(`Model "${schemaKey}" cannot claim a row without collaboration wiring.`, { code: 'model_claim_not_configured' });
|
|
247
|
+
}
|
|
248
|
+
autoLease = await collaboration.createIntent({
|
|
249
|
+
target: {
|
|
250
|
+
model: schemaKey,
|
|
251
|
+
id,
|
|
252
|
+
...(claim.field ? { field: claim.field } : {}),
|
|
253
|
+
...(claim.path ? { path: claim.path } : {}),
|
|
254
|
+
...(claim.range ? { range: claim.range } : {}),
|
|
255
|
+
...(claimMeta(claim) ? { meta: claimMeta(claim) } : {}),
|
|
256
|
+
},
|
|
257
|
+
action: claim.action ?? 'creating',
|
|
258
|
+
ttl: claim.ttl,
|
|
259
|
+
queue: claim.wait !== false,
|
|
260
|
+
maxQueueDepth: claim.maxQueueDepth,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
221
263
|
const model = new ModelClass({
|
|
222
|
-
id
|
|
223
|
-
...data,
|
|
264
|
+
id,
|
|
265
|
+
...params.data,
|
|
224
266
|
createdAt: new Date(),
|
|
225
267
|
updatedAt: new Date(),
|
|
226
268
|
});
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
269
|
+
const effective = {
|
|
270
|
+
...opts,
|
|
271
|
+
...(autoLease ? { intent: autoLease } : {}),
|
|
272
|
+
...(isClaimHandle(claim) ? { intent: { id: claim.claimId } } : {}),
|
|
273
|
+
};
|
|
274
|
+
try {
|
|
275
|
+
syncClient.add(model, effective);
|
|
276
|
+
await waitForMutation(model, effective);
|
|
277
|
+
return modelAsRow(model);
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
await autoLease?.release().catch(() => { });
|
|
281
|
+
}
|
|
230
282
|
}),
|
|
231
|
-
update: guard(async (
|
|
283
|
+
update: guard(async (params) => {
|
|
284
|
+
const autoClaim = params.claim && !isClaimHandle(params.claim) ? params.claim : null;
|
|
285
|
+
if (autoClaim) {
|
|
286
|
+
const handle = await takeClaim({ ...autoClaim, id: params.id });
|
|
287
|
+
try {
|
|
288
|
+
return await operations.update({ ...params, claim: handle });
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
await handle.release();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const { id } = params;
|
|
232
295
|
const model = objectPool.get(id);
|
|
233
296
|
if (!model)
|
|
234
297
|
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
235
298
|
// If we hold a claim on this row, guard the write with its snapshot
|
|
236
299
|
// watermark + lease so it's stale-rejected and attributed to the claim.
|
|
237
300
|
const claimed = activeClaims.get(id);
|
|
301
|
+
const opts = mutationOptions(params);
|
|
302
|
+
const handleIntent = isClaimHandle(params.claim)
|
|
303
|
+
? { id: params.claim.claimId }
|
|
304
|
+
: undefined;
|
|
238
305
|
const effective = claimed
|
|
239
306
|
? {
|
|
240
307
|
wait: 'confirmed',
|
|
241
308
|
readAt: claimed.snapshot.stamp,
|
|
242
309
|
onStale: 'reject',
|
|
243
310
|
intent: claimed.lease,
|
|
244
|
-
...
|
|
311
|
+
...opts,
|
|
245
312
|
}
|
|
246
|
-
:
|
|
247
|
-
|
|
313
|
+
: {
|
|
314
|
+
...opts,
|
|
315
|
+
...(handleIntent ? { intent: handleIntent } : {}),
|
|
316
|
+
};
|
|
317
|
+
// Local user update: `applyChanges` keeps change tracking ON so
|
|
318
|
+
// the edited fields land in `modifiedProperties` and actually get
|
|
319
|
+
// sent to the server. (`updateFromData` is the hydration path and
|
|
320
|
+
// would discard the tracking → empty `input: {}` no-op mutation.)
|
|
321
|
+
model.applyChanges(params.data);
|
|
248
322
|
syncClient.update(model, effective);
|
|
249
323
|
await waitForMutation(model, effective);
|
|
250
324
|
return modelAsRow(model);
|
|
251
325
|
}),
|
|
252
|
-
delete: guard(async (
|
|
326
|
+
delete: guard(async (params) => {
|
|
327
|
+
const autoClaim = params.claim && !isClaimHandle(params.claim) ? params.claim : null;
|
|
328
|
+
if (autoClaim) {
|
|
329
|
+
const handle = await takeClaim({ ...autoClaim, id: params.id });
|
|
330
|
+
try {
|
|
331
|
+
await operations.delete({ ...params, claim: handle });
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
await handle.release();
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const { id } = params;
|
|
253
339
|
const model = objectPool.get(id);
|
|
254
340
|
if (!model)
|
|
255
341
|
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
256
|
-
|
|
257
|
-
|
|
342
|
+
const claimed = activeClaims.get(id);
|
|
343
|
+
const opts = mutationOptions(params);
|
|
344
|
+
const handleIntent = isClaimHandle(params.claim)
|
|
345
|
+
? { id: params.claim.claimId }
|
|
346
|
+
: undefined;
|
|
347
|
+
const effective = claimed
|
|
348
|
+
? {
|
|
349
|
+
wait: 'confirmed',
|
|
350
|
+
readAt: claimed.snapshot.stamp,
|
|
351
|
+
onStale: 'reject',
|
|
352
|
+
intent: claimed.lease,
|
|
353
|
+
...opts,
|
|
354
|
+
}
|
|
355
|
+
: {
|
|
356
|
+
...opts,
|
|
357
|
+
...(handleIntent ? { intent: handleIntent } : {}),
|
|
358
|
+
};
|
|
359
|
+
syncClient.delete(model, effective);
|
|
360
|
+
await waitForMutation(model, effective);
|
|
258
361
|
}),
|
|
259
362
|
// `claim` is a callable namespace (take a claim) carrying the coordination
|
|
260
363
|
// readers (`claim.state` / `claim.queue` / `claim.release` / `claim.reorder`).
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createAbloHttpClient` — a STATELESS, typed HTTP client for server-side actors
|
|
3
|
+
* (agents, workers, serverless), modelled on `@liveblocks/node` / the Stripe
|
|
4
|
+
* server SDK / Netflix Conductor workers: it talks to Ablo over plain HTTP with
|
|
5
|
+
* the credential as identity, and holds **no WebSocket and no connection state**.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists (docs/plans/agent-transport-event-driven.md): the stateful
|
|
8
|
+
* `Ablo({ schema })` client is for INTERACTIVE participants — it opens a
|
|
9
|
+
* WebSocket and seeds its identity (userId/orgId) during the connect/bootstrap
|
|
10
|
+
* step, then routes writes through a `TransactionQueue` that drops mutations
|
|
11
|
+
* until that identity exists. A reactive agent has no socket, so that identity is
|
|
12
|
+
* never seeded and writes drop. The proven fix (unanimous across Liveblocks,
|
|
13
|
+
* Stripe, PlanetScale, Conductor, Better Auth) is NOT to de-socket the stateful
|
|
14
|
+
* client — it's a separate stateless client where the credential carries identity
|
|
15
|
+
* and the SERVER resolves it per request.
|
|
16
|
+
*
|
|
17
|
+
* Ablo already has that stateless surface: `Ablo({ schema: null })` returns the
|
|
18
|
+
* protocol client (`createProtocolClient` → `AbloApi`), which commits via
|
|
19
|
+
* `POST /v1/commits` and reads via the HTTP `ApiClient`, authenticating with the
|
|
20
|
+
* Bearer token on every request. Its only ergonomic gap is that model access is
|
|
21
|
+
* string-keyed (`api.model('slides')`) rather than typed (`api.slides`). This
|
|
22
|
+
* wraps it in a typed proxy facade so server code gets the SAME `client.<model>`
|
|
23
|
+
* surface as the browser client — typed proxies, stateless transport.
|
|
24
|
+
*/
|
|
25
|
+
import { type AbloApiClientOptions, type TaskCreateOptions } from './ApiClient.js';
|
|
26
|
+
import type { CommitReceipt, CommitResource, HttpClaimApi, ModelRead, ModelReadOptions, Turn } from './Ablo.js';
|
|
27
|
+
import type { ModelCreateParams, ModelDeleteParams, ModelLoadOptions, ModelRetrieveParams, ModelUpdateParams } from './createModelProxy.js';
|
|
28
|
+
import type { Schema, SchemaRecord, InferModel, InferCreate } from '../schema/schema.js';
|
|
29
|
+
export interface AbloHttpClientOptions<S extends SchemaRecord> extends Omit<AbloApiClientOptions, 'schema'> {
|
|
30
|
+
/** The schema — used for TYPING only (typed model proxies); never sent or used at runtime. */
|
|
31
|
+
readonly schema: Schema<S>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The per-model HTTP surface — exactly what a stateless client can do over
|
|
35
|
+
* request/response: reads (`retrieve`/`list`), writes (`create`/`update`/`delete`),
|
|
36
|
+
* and the durable-lease claim plane (`claim` — acquire/hold/release). It does NOT
|
|
37
|
+
* include `get`/`getAll`/`getCount` (local synced-pool reads) or `onChange` (live
|
|
38
|
+
* subscription); those need the stateful plane and are absent BY TYPE here.
|
|
39
|
+
*/
|
|
40
|
+
export interface HttpModelClient<T, C = T> {
|
|
41
|
+
retrieve(params: ModelRetrieveParams & ModelReadOptions): Promise<ModelRead<T>>;
|
|
42
|
+
list(options?: ModelLoadOptions<T>): Promise<T[]>;
|
|
43
|
+
create(params: ModelCreateParams<T, C>): Promise<CommitReceipt>;
|
|
44
|
+
update(params: ModelUpdateParams<C>): Promise<CommitReceipt>;
|
|
45
|
+
delete(params: ModelDeleteParams<T>): Promise<CommitReceipt>;
|
|
46
|
+
claim: HttpClaimApi<T>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The honest type of the stateless HTTP client: typed model proxies (the
|
|
50
|
+
* request/response subset) + `commits` + `beginTurn` + `dispose`. Reaching for a
|
|
51
|
+
* stateful-only capability (`get`/`getAll`/`getCount`, `onChange`,
|
|
52
|
+
* `claim.state`/`queue`/`reorder`) is a COMPILE error here, not a latent runtime
|
|
53
|
+
* `undefined` — the type matches what the transport can actually do.
|
|
54
|
+
*/
|
|
55
|
+
export type AbloHttpClient<S extends SchemaRecord> = {
|
|
56
|
+
readonly [K in keyof S & string]: HttpModelClient<InferModel<Schema<S>, K>, InferCreate<Schema<S>, K>>;
|
|
57
|
+
} & {
|
|
58
|
+
readonly commits: CommitResource;
|
|
59
|
+
beginTurn(options: TaskCreateOptions): Promise<Turn>;
|
|
60
|
+
dispose(): Promise<void>;
|
|
61
|
+
/** Resolve the bearer credential this client authenticates with (see `AbloApi.getAuthToken`). */
|
|
62
|
+
getAuthToken(): Promise<string | null>;
|
|
63
|
+
/** String-keyed model accessor (for dynamic model names). */
|
|
64
|
+
model<T = Record<string, unknown>>(name: string): HttpModelClient<T>;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Stateless, typed HTTP client. Each `client.<model>` resolves to the protocol
|
|
68
|
+
* client's `model(name)`; `commits`, `beginTurn`, `tasks`, `dispose`, etc. pass
|
|
69
|
+
* through. No socket is ever opened; identity is the Bearer credential.
|
|
70
|
+
*/
|
|
71
|
+
export declare function createAbloHttpClient<S extends SchemaRecord>(options: AbloHttpClientOptions<S>): AbloHttpClient<S>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createAbloHttpClient` — a STATELESS, typed HTTP client for server-side actors
|
|
3
|
+
* (agents, workers, serverless), modelled on `@liveblocks/node` / the Stripe
|
|
4
|
+
* server SDK / Netflix Conductor workers: it talks to Ablo over plain HTTP with
|
|
5
|
+
* the credential as identity, and holds **no WebSocket and no connection state**.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists (docs/plans/agent-transport-event-driven.md): the stateful
|
|
8
|
+
* `Ablo({ schema })` client is for INTERACTIVE participants — it opens a
|
|
9
|
+
* WebSocket and seeds its identity (userId/orgId) during the connect/bootstrap
|
|
10
|
+
* step, then routes writes through a `TransactionQueue` that drops mutations
|
|
11
|
+
* until that identity exists. A reactive agent has no socket, so that identity is
|
|
12
|
+
* never seeded and writes drop. The proven fix (unanimous across Liveblocks,
|
|
13
|
+
* Stripe, PlanetScale, Conductor, Better Auth) is NOT to de-socket the stateful
|
|
14
|
+
* client — it's a separate stateless client where the credential carries identity
|
|
15
|
+
* and the SERVER resolves it per request.
|
|
16
|
+
*
|
|
17
|
+
* Ablo already has that stateless surface: `Ablo({ schema: null })` returns the
|
|
18
|
+
* protocol client (`createProtocolClient` → `AbloApi`), which commits via
|
|
19
|
+
* `POST /v1/commits` and reads via the HTTP `ApiClient`, authenticating with the
|
|
20
|
+
* Bearer token on every request. Its only ergonomic gap is that model access is
|
|
21
|
+
* string-keyed (`api.model('slides')`) rather than typed (`api.slides`). This
|
|
22
|
+
* wraps it in a typed proxy facade so server code gets the SAME `client.<model>`
|
|
23
|
+
* surface as the browser client — typed proxies, stateless transport.
|
|
24
|
+
*/
|
|
25
|
+
import { createProtocolClient, } from './ApiClient.js';
|
|
26
|
+
/**
|
|
27
|
+
* Members of the underlying `AbloApi` that pass straight through the facade.
|
|
28
|
+
* Deliberately EXCLUDES the resource names that collide with common schema model
|
|
29
|
+
* names — `tasks`, `intents`, `capabilities`, `agent` — so `client.tasks` resolves
|
|
30
|
+
* to the schema model `tasks`, not the protocol `TaskResource`. Only lifecycle +
|
|
31
|
+
* the genuinely-protocol methods an agent uses pass through.
|
|
32
|
+
*/
|
|
33
|
+
const PROTOCOL_MEMBERS = new Set([
|
|
34
|
+
'ready',
|
|
35
|
+
'waitForFlush',
|
|
36
|
+
'dispose',
|
|
37
|
+
'purge',
|
|
38
|
+
'commits',
|
|
39
|
+
'beginTurn',
|
|
40
|
+
'model',
|
|
41
|
+
'getAuthToken',
|
|
42
|
+
]);
|
|
43
|
+
/**
|
|
44
|
+
* Stateless, typed HTTP client. Each `client.<model>` resolves to the protocol
|
|
45
|
+
* client's `model(name)`; `commits`, `beginTurn`, `tasks`, `dispose`, etc. pass
|
|
46
|
+
* through. No socket is ever opened; identity is the Bearer credential.
|
|
47
|
+
*/
|
|
48
|
+
export function createAbloHttpClient(options) {
|
|
49
|
+
// The schema is type-level only; the protocol client is schema-agnostic.
|
|
50
|
+
const { schema: _schema, ...rest } = options;
|
|
51
|
+
const api = createProtocolClient({ ...rest, schema: null });
|
|
52
|
+
const facade = new Proxy(api, {
|
|
53
|
+
get(target, prop) {
|
|
54
|
+
if (typeof prop !== 'string')
|
|
55
|
+
return Reflect.get(target, prop);
|
|
56
|
+
// Real protocol members pass through unchanged.
|
|
57
|
+
if (PROTOCOL_MEMBERS.has(prop) && prop in target)
|
|
58
|
+
return Reflect.get(target, prop);
|
|
59
|
+
// Anything else is a typed model accessor → the string-keyed protocol model
|
|
60
|
+
// (which implements retrieve/list/create/update/delete/claim — every method
|
|
61
|
+
// `HttpModelClient` declares).
|
|
62
|
+
return api.model(prop);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
// One boundary cast — and now an HONEST one: `AbloHttpClient<S>` declares only
|
|
66
|
+
// what `api.model()` + the passed-through protocol members actually implement,
|
|
67
|
+
// so there is no method on this type that fails at runtime.
|
|
68
|
+
return facade;
|
|
69
|
+
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { type RefreshScheduler } from '../auth/index.js';
|
|
23
23
|
import type { BootstrapHelper } from '../sync/BootstrapHelper.js';
|
|
24
24
|
import type { SyncLogger } from '../interfaces/index.js';
|
|
25
|
+
import type { AuthCredentialSource } from '../auth/credentialSource.js';
|
|
25
26
|
import type { ApiKeySetter } from './auth.js';
|
|
26
27
|
export interface IdentityResolveInput {
|
|
27
28
|
readonly options: {
|
|
@@ -42,13 +43,8 @@ export interface IdentityResolveInput {
|
|
|
42
43
|
readonly configuredApiKey: string | ApiKeySetter | null;
|
|
43
44
|
readonly configuredAuthToken: string | null;
|
|
44
45
|
readonly bootstrapHelper: BootstrapHelper;
|
|
46
|
+
readonly auth: AuthCredentialSource;
|
|
45
47
|
readonly logger: SyncLogger;
|
|
46
|
-
/**
|
|
47
|
-
* Called when the hosted-cloud refresh scheduler rotates the
|
|
48
|
-
* capability token — caller forwards the new token to the live
|
|
49
|
-
* WebSocket via `ws.setCapabilityToken(...)`.
|
|
50
|
-
*/
|
|
51
|
-
readonly applyRotatedToken: (token: string) => void;
|
|
52
48
|
}
|
|
53
49
|
export interface ResolvedIdentity {
|
|
54
50
|
readonly userId: string;
|
package/dist/client/identity.js
CHANGED
|
@@ -23,11 +23,43 @@ import { AbloAuthenticationError } from '../errors.js';
|
|
|
23
23
|
import { exchangeApiKey } from '../auth/index.js';
|
|
24
24
|
import { resolveIdentity } from '../auth/index.js';
|
|
25
25
|
import { createRefreshScheduler, } from '../auth/index.js';
|
|
26
|
-
import { resolveApiKeyValue } from './auth.js';
|
|
26
|
+
import { resolveApiKeyValue, resolveBootstrapBaseUrl } from './auth.js';
|
|
27
27
|
export async function resolveParticipantIdentity(input) {
|
|
28
|
-
const { options, internalOptions, url, kind, configuredApiKey, configuredAuthToken, bootstrapHelper,
|
|
28
|
+
const { options, internalOptions, url, kind, configuredApiKey, configuredAuthToken, bootstrapHelper, auth, logger, } = input;
|
|
29
29
|
const apiKeyValue = await resolveApiKeyValue(configuredApiKey);
|
|
30
30
|
const initialCapToken = options.capabilityToken ?? configuredAuthToken ?? undefined;
|
|
31
|
+
// Branch 0: publishable key (`pk_`) — a long-lived, browser-safe, READ-ONLY
|
|
32
|
+
// project key. Unlike a secret `sk_` (Branch 1), it is used DIRECTLY as the
|
|
33
|
+
// bearer and is NEVER exchanged for a short-lived capability — so it never
|
|
34
|
+
// expires and there is nothing to refresh (no `credential_stale`, no
|
|
35
|
+
// wake-from-sleep re-mint). The sync-server's `apiKeyProvider` resolves the
|
|
36
|
+
// org + read-only scope from the key itself; we still call `/auth/identity`
|
|
37
|
+
// (authenticated by the `pk_` bearer) to learn the account scope + syncGroups
|
|
38
|
+
// for the bootstrap cache. Plain `startsWith` check because the `keys` module
|
|
39
|
+
// is node-only (`node:crypto`) and must not enter the browser bundle.
|
|
40
|
+
if (apiKeyValue && apiKeyValue.startsWith('pk_') && !options.capabilityToken) {
|
|
41
|
+
const baseUrl = resolveBootstrapBaseUrl({
|
|
42
|
+
url,
|
|
43
|
+
bootstrapBaseUrl: options.bootstrapBaseUrl,
|
|
44
|
+
});
|
|
45
|
+
const identity = await resolveIdentity({ baseUrl, authToken: apiKeyValue });
|
|
46
|
+
const callerGroups = options.syncGroups ?? [];
|
|
47
|
+
const mergedSyncGroups = callerGroups.length > 0
|
|
48
|
+
? [...new Set([...callerGroups, ...identity.syncGroups])]
|
|
49
|
+
: identity.syncGroups;
|
|
50
|
+
bootstrapHelper.setCacheScope(identity.accountScope);
|
|
51
|
+
bootstrapHelper.setSyncGroups(mergedSyncGroups);
|
|
52
|
+
auth.setAuthToken(apiKeyValue);
|
|
53
|
+
return {
|
|
54
|
+
userId: identity.participantId,
|
|
55
|
+
accountScope: identity.accountScope,
|
|
56
|
+
teamIds: undefined,
|
|
57
|
+
capabilityToken: apiKeyValue,
|
|
58
|
+
syncGroups: mergedSyncGroups,
|
|
59
|
+
participantKind: identity.participantKind,
|
|
60
|
+
refreshScheduler: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
31
63
|
// Branch 1: hosted-cloud (apiKey only, no caller-supplied capability token)
|
|
32
64
|
if (apiKeyValue && !options.capabilityToken) {
|
|
33
65
|
return resolveHosted({
|
|
@@ -37,8 +69,8 @@ export async function resolveParticipantIdentity(input) {
|
|
|
37
69
|
kind,
|
|
38
70
|
options,
|
|
39
71
|
bootstrapHelper,
|
|
72
|
+
auth,
|
|
40
73
|
logger,
|
|
41
|
-
applyRotatedToken,
|
|
42
74
|
});
|
|
43
75
|
}
|
|
44
76
|
// Branch 2: self-derived (capability token present, identity unknown)
|
|
@@ -58,7 +90,12 @@ export async function resolveParticipantIdentity(input) {
|
|
|
58
90
|
'missing or expired. Ensure `getToken()` returns a valid token, or ' +
|
|
59
91
|
'pass `apiKey` / `capabilityToken`.', { code: 'session_expired' });
|
|
60
92
|
}
|
|
61
|
-
|
|
93
|
+
// Single source of truth for the http(s) base — coerces ws/wss → http/https
|
|
94
|
+
// even when `bootstrapBaseUrl` is an explicit override (see auth.ts).
|
|
95
|
+
const baseUrl = resolveBootstrapBaseUrl({
|
|
96
|
+
url,
|
|
97
|
+
bootstrapBaseUrl: options.bootstrapBaseUrl,
|
|
98
|
+
});
|
|
62
99
|
const identity = await resolveIdentity({
|
|
63
100
|
baseUrl,
|
|
64
101
|
authToken: initialCapToken,
|
|
@@ -80,7 +117,7 @@ export async function resolveParticipantIdentity(input) {
|
|
|
80
117
|
: identity.syncGroups;
|
|
81
118
|
bootstrapHelper.setCacheScope(identity.accountScope);
|
|
82
119
|
bootstrapHelper.setSyncGroups(mergedSyncGroups);
|
|
83
|
-
|
|
120
|
+
auth.setAuthToken(initialCapToken);
|
|
84
121
|
return {
|
|
85
122
|
userId: identity.participantId,
|
|
86
123
|
accountScope: identity.accountScope,
|
|
@@ -97,7 +134,7 @@ export async function resolveParticipantIdentity(input) {
|
|
|
97
134
|
const accountScope = internalOptions.organizationId;
|
|
98
135
|
bootstrapHelper.setCacheScope(accountScope);
|
|
99
136
|
bootstrapHelper.setSyncGroups(options.syncGroups);
|
|
100
|
-
|
|
137
|
+
auth.setAuthToken(initialCapToken);
|
|
101
138
|
return {
|
|
102
139
|
userId,
|
|
103
140
|
accountScope,
|
|
@@ -111,8 +148,10 @@ export async function resolveParticipantIdentity(input) {
|
|
|
111
148
|
async function resolveHosted(input) {
|
|
112
149
|
// Pure managed-cloud shape: `Ablo({schema, apiKey})`. Server returns
|
|
113
150
|
// scope + userMeta; SDK populates internals.
|
|
114
|
-
const baseUrl =
|
|
115
|
-
|
|
151
|
+
const baseUrl = resolveBootstrapBaseUrl({
|
|
152
|
+
url: input.url,
|
|
153
|
+
bootstrapBaseUrl: input.options.bootstrapBaseUrl,
|
|
154
|
+
});
|
|
116
155
|
const exchangeArgs = {
|
|
117
156
|
baseUrl,
|
|
118
157
|
participantKind: (input.kind === 'agent' ? 'agent' : 'system'),
|
|
@@ -126,7 +165,7 @@ async function resolveHosted(input) {
|
|
|
126
165
|
});
|
|
127
166
|
input.bootstrapHelper.setCacheScope(exchange.scope.organizationId);
|
|
128
167
|
input.bootstrapHelper.setSyncGroups(exchange.scope.syncGroups);
|
|
129
|
-
input.
|
|
168
|
+
input.auth.setAuthToken(exchange.token);
|
|
130
169
|
// Cap tokens have a server-set TTL (3600s by default). Without
|
|
131
170
|
// proactive refresh the WS would either get force-closed at expiry
|
|
132
171
|
// or fail its next reconnect with 401. The scheduler re-mints
|
|
@@ -146,8 +185,7 @@ async function resolveHosted(input) {
|
|
|
146
185
|
...exchangeArgs,
|
|
147
186
|
apiKey: freshApiKey,
|
|
148
187
|
});
|
|
149
|
-
input.
|
|
150
|
-
input.applyRotatedToken(next.token);
|
|
188
|
+
input.auth.setAuthToken(next.token);
|
|
151
189
|
return { expiresAtMs: Date.parse(next.expiresAt) };
|
|
152
190
|
},
|
|
153
191
|
onError: (err) => {
|
package/dist/client/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
export { Ablo, computeFKDepthPriority, type AbloOptions, type InternalAbloOptions, type ClaimedOptions, type IfClaimedPolicy, type IntentWaitOptions, type ModelCountOptions, type ModelListOptions, type ModelListScope, type ModelLoadOptions, type ModelOperations, type ModelReadOptions, } from './Ablo.js';
|
|
33
|
+
export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './auth.js';
|
|
33
34
|
export type { AbloPersistence } from './persistence.js';
|
|
34
35
|
export type { AbloApi, AbloApiClientOptions, AbloApiIntents, Agent, AgentIntentInput, AgentIntentOptions, AgentOptions, AgentModelClient, AgentModelReadOptions, AgentModelMutationOptions, AgentRunContext, AgentRunDone, AgentRunFailed, AgentRunCancelled, AgentRunOptions, AgentRunResult, AgentRunStatus, Capability, CapabilityCreateOptions, CapabilityParticipantKind, CapabilityRecord, CapabilityResource, CapabilityRevocation, CapabilityScope, Task, TaskCloseOptions, TaskCloseResult, TaskCreateOptions, TaskResource, } from './ApiClient.js';
|
|
35
36
|
export type { EngineParticipant, JoinedParticipant, ParticipantJoinOptions, ParticipantManager, ParticipantScope, ParticipantStatus, ScopedIntents, ScopedPresence, } from '../sync/participants.js';
|
package/dist/client/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export interface RegisterDataSourceInput {
|
|
|
3
3
|
readonly baseUrl: string;
|
|
4
4
|
/** Secret key (`sk_…`) used to authenticate + derive the org. */
|
|
5
5
|
readonly apiKey: string | null;
|
|
6
|
-
/**
|
|
6
|
+
/** Postgres connection string for the direct connector. */
|
|
7
7
|
readonly databaseUrl: string;
|
|
8
8
|
/** Optional Postgres schema (defaults server-side to `public`). */
|
|
9
9
|
readonly schema?: string;
|
|
@@ -11,8 +11,8 @@ export interface RegisterDataSourceInput {
|
|
|
11
11
|
readonly fetchImpl?: typeof fetch;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* POST the connection string to the self-serve
|
|
15
|
-
* success (the org is now a dedicated tenant pointed at this DB); throws an
|
|
14
|
+
* POST the connection string to the self-serve direct connector route. Resolves
|
|
15
|
+
* on success (the org is now a dedicated tenant pointed at this DB); throws an
|
|
16
16
|
* `AbloError` with `datasource_registration_failed` otherwise so `ready()`
|
|
17
17
|
* surfaces it instead of silently bootstrapping against the wrong store.
|
|
18
18
|
*/
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Self-serve
|
|
2
|
+
* Self-serve direct-Postgres connector registration.
|
|
3
|
+
*
|
|
4
|
+
* Historical note: this module name says "DataSource", but this path registers
|
|
5
|
+
* a direct database URL. It is not the signed `dataSource(...)` endpoint path.
|
|
3
6
|
*
|
|
4
7
|
* When a client is constructed with `databaseUrl`, the SDK registers that
|
|
5
|
-
* connection string
|
|
6
|
-
*
|
|
7
|
-
* synced rows back into it (dedicated/BYO tenant).
|
|
8
|
+
* connection string BEFORE bootstrap so the server resolves the org's data plane
|
|
9
|
+
* to that direct connector.
|
|
8
10
|
*
|
|
9
11
|
* The org is derived server-side from the API key — the caller never sends an
|
|
10
12
|
* organization id. The connection string is sent once over TLS and is never
|
|
@@ -13,14 +15,14 @@
|
|
|
13
15
|
*/
|
|
14
16
|
import { AbloError } from '../errors.js';
|
|
15
17
|
/**
|
|
16
|
-
* POST the connection string to the self-serve
|
|
17
|
-
* success (the org is now a dedicated tenant pointed at this DB); throws an
|
|
18
|
+
* POST the connection string to the self-serve direct connector route. Resolves
|
|
19
|
+
* on success (the org is now a dedicated tenant pointed at this DB); throws an
|
|
18
20
|
* `AbloError` with `datasource_registration_failed` otherwise so `ready()`
|
|
19
21
|
* surfaces it instead of silently bootstrapping against the wrong store.
|
|
20
22
|
*/
|
|
21
23
|
export async function registerDataSource(input) {
|
|
22
24
|
if (!input.apiKey) {
|
|
23
|
-
throw new AbloError('databaseUrl requires an apiKey to register the
|
|
25
|
+
throw new AbloError('databaseUrl requires an apiKey to register the direct Postgres connector (the org is derived from the key).', { code: 'datasource_registration_failed' });
|
|
24
26
|
}
|
|
25
27
|
const doFetch = input.fetchImpl ?? fetch;
|
|
26
28
|
const endpoint = `${input.baseUrl.replace(/\/+$/, '')}/v1/datasource`;
|
|
@@ -39,7 +41,7 @@ export async function registerDataSource(input) {
|
|
|
39
41
|
});
|
|
40
42
|
}
|
|
41
43
|
catch (cause) {
|
|
42
|
-
throw new AbloError('Could not reach the Ablo API to register
|
|
44
|
+
throw new AbloError('Could not reach the Ablo API to register the direct Postgres connector.', {
|
|
43
45
|
code: 'datasource_registration_failed',
|
|
44
46
|
cause,
|
|
45
47
|
});
|
|
@@ -52,6 +54,6 @@ export async function registerDataSource(input) {
|
|
|
52
54
|
catch {
|
|
53
55
|
// ignore body read failures — the status alone is enough to fail loud
|
|
54
56
|
}
|
|
55
|
-
throw new AbloError(`
|
|
57
|
+
throw new AbloError(`Direct Postgres connector registration failed (HTTP ${response.status}). ${detail}`, { code: 'datasource_registration_failed', httpStatus: response.status });
|
|
56
58
|
}
|
|
57
59
|
}
|