@abloatai/ablo 0.7.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 +72 -1
- package/README.md +80 -66
- package/dist/BaseSyncedStore.d.ts +73 -0
- package/dist/BaseSyncedStore.js +179 -5
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/SyncEngineContext.d.ts +2 -1
- package/dist/SyncEngineContext.js +5 -3
- package/dist/agent/session.js +6 -5
- 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 +26 -36
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +259 -33
- package/dist/client/Ablo.js +276 -73
- package/dist/client/ApiClient.d.ts +52 -4
- package/dist/client/ApiClient.js +236 -66
- package/dist/client/auth.d.ts +21 -2
- package/dist/client/auth.js +77 -5
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +187 -79
- package/dist/client/createModelProxy.js +203 -68
- 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 +63 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +19 -0
- package/dist/client/registerDataSource.js +59 -0
- package/dist/client/validateAbloOptions.d.ts +2 -1
- package/dist/client/validateAbloOptions.js +8 -7
- 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 +92 -1
- package/dist/errorCodes.js +139 -7
- package/dist/errors.d.ts +54 -3
- package/dist/errors.js +192 -44
- package/dist/index.d.ts +23 -10
- package/dist/index.js +21 -8
- package/dist/keys/index.d.ts +76 -0
- package/dist/keys/index.js +171 -0
- 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 +22 -14
- package/dist/react/AbloProvider.d.ts +23 -101
- package/dist/react/AbloProvider.js +61 -103
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/SyncGroupProvider.d.ts +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +3 -2
- package/dist/react/useAblo.d.ts +4 -4
- package/dist/react/useAblo.js +10 -5
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/react/useReactive.js +16 -3
- 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 +7 -3
- package/dist/schema/serialize.js +6 -2
- 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 +56 -42
- package/dist/sync/ConnectionManager.d.ts +57 -1
- package/dist/sync/ConnectionManager.js +186 -11
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +241 -41
- package/dist/sync/NetworkProbe.d.ts +60 -18
- package/dist/sync/NetworkProbe.js +121 -23
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +113 -89
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/sync/participants.js +5 -2
- package/dist/transactions/TransactionQueue.js +13 -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-keys.md +5 -5
- package/docs/api.md +125 -65
- package/docs/audit.md +16 -9
- package/docs/cli.md +57 -47
- package/docs/client-behavior.md +54 -40
- package/docs/coordination.md +66 -80
- package/docs/data-sources.md +56 -34
- package/docs/examples/agent-human.md +74 -28
- package/docs/examples/ai-sdk-tool.md +29 -22
- package/docs/examples/existing-python-backend.md +41 -26
- package/docs/examples/nextjs.md +32 -17
- package/docs/examples/scoped-agent.md +43 -28
- package/docs/examples/server-agent.md +40 -15
- package/docs/guarantees.md +38 -27
- package/docs/identity.md +65 -59
- package/docs/index.md +30 -19
- package/docs/integration-guide.md +78 -78
- package/docs/interaction-model.md +43 -35
- package/docs/mcp/claude-code.md +11 -19
- package/docs/mcp/cursor.md +7 -25
- package/docs/mcp/windsurf.md +7 -20
- package/docs/mcp.md +103 -26
- package/docs/quickstart.md +63 -61
- package/docs/react.md +24 -16
- package/docs/roadmap.md +13 -13
- package/docs/schema-contract.md +111 -0
- package/docs/the-loop.md +21 -0
- package/examples/README.md +8 -4
- package/examples/data-source/README.md +10 -7
- package/examples/data-source/customer-server.ts +27 -25
- package/examples/data-source/run.ts +4 -3
- package/examples/quickstart.ts +1 -1
- package/llms.txt +55 -21
- package/package.json +48 -3
package/dist/client/ApiClient.js
CHANGED
|
@@ -171,32 +171,36 @@ export function createProtocolClient(options) {
|
|
|
171
171
|
function createAgentModelClient(agentClient, name) {
|
|
172
172
|
const base = agentClient.model(name);
|
|
173
173
|
return {
|
|
174
|
-
retrieve(
|
|
174
|
+
retrieve(params) {
|
|
175
175
|
// Reads are never blocked by a claim (coordination.md): a claim
|
|
176
176
|
// serializes WRITERS, not readers. So — unlike the create/update/
|
|
177
177
|
// delete paths below — retrieve does NOT apply the agent claimed
|
|
178
178
|
// default; options pass through and the read path's `'return'`
|
|
179
179
|
// default keeps a claimed row readable. A caller can still opt into
|
|
180
180
|
// gating with an explicit `ifClaimed` (developer's choice).
|
|
181
|
-
return base.retrieve(
|
|
181
|
+
return base.retrieve(params);
|
|
182
182
|
},
|
|
183
|
-
create(
|
|
184
|
-
const id =
|
|
185
|
-
return withAgentIntent(agentClient, name, id,
|
|
186
|
-
...stripAgentRuntimeOptions(
|
|
183
|
+
create(params) {
|
|
184
|
+
const id = params.id ?? createModelId();
|
|
185
|
+
return withAgentIntent(agentClient, name, id, params, (commitIntent) => base.create({
|
|
186
|
+
...stripAgentRuntimeOptions(params),
|
|
187
187
|
id,
|
|
188
|
+
data: params.data,
|
|
188
189
|
intent: commitIntent,
|
|
189
190
|
}));
|
|
190
191
|
},
|
|
191
|
-
update(
|
|
192
|
-
return withAgentIntent(agentClient, name, id,
|
|
193
|
-
...stripAgentRuntimeOptions(
|
|
192
|
+
update(params) {
|
|
193
|
+
return withAgentIntent(agentClient, name, params.id, params, (commitIntent) => base.update({
|
|
194
|
+
...stripAgentRuntimeOptions(params),
|
|
195
|
+
id: params.id,
|
|
196
|
+
data: params.data,
|
|
194
197
|
intent: commitIntent,
|
|
195
198
|
}));
|
|
196
199
|
},
|
|
197
|
-
delete(
|
|
198
|
-
return withAgentIntent(agentClient, name, id,
|
|
199
|
-
...stripAgentRuntimeOptions(
|
|
200
|
+
delete(params) {
|
|
201
|
+
return withAgentIntent(agentClient, name, params.id, params, (commitIntent) => base.delete({
|
|
202
|
+
...stripAgentRuntimeOptions(params),
|
|
203
|
+
id: params.id,
|
|
200
204
|
intent: commitIntent,
|
|
201
205
|
}));
|
|
202
206
|
},
|
|
@@ -439,6 +443,35 @@ export function createProtocolClient(options) {
|
|
|
439
443
|
activeSessionsClosed: body.activeSessionsClosed,
|
|
440
444
|
};
|
|
441
445
|
},
|
|
446
|
+
async rotate(id, rotateOptions = {}) {
|
|
447
|
+
const graceSeconds = rotateOptions.graceSeconds ??
|
|
448
|
+
(rotateOptions.grace !== undefined ? toSeconds(rotateOptions.grace) : undefined);
|
|
449
|
+
const leaseSeconds = rotateOptions.leaseSeconds ??
|
|
450
|
+
(rotateOptions.lease !== undefined ? toSeconds(rotateOptions.lease) : undefined);
|
|
451
|
+
const body = await requestJson(`/v1/capabilities/${encodeURIComponent(id)}/rotate`, {
|
|
452
|
+
method: 'POST',
|
|
453
|
+
body: JSON.stringify({
|
|
454
|
+
...(graceSeconds !== undefined ? { graceSeconds } : {}),
|
|
455
|
+
...(leaseSeconds !== undefined ? { ttlSeconds: leaseSeconds } : {}),
|
|
456
|
+
}),
|
|
457
|
+
});
|
|
458
|
+
const newId = body.capabilityId ?? body.id;
|
|
459
|
+
if (!newId) {
|
|
460
|
+
throw new AbloValidationError('Capability rotate response did not include an id.', { code: 'capability_id_missing' });
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
id: newId,
|
|
464
|
+
token: body.token,
|
|
465
|
+
expiresAt: body.expiresAt,
|
|
466
|
+
organizationId: body.organizationId,
|
|
467
|
+
scope: body.scope,
|
|
468
|
+
rotatedFrom: {
|
|
469
|
+
id: body.rotatedFrom.capabilityId ?? body.rotatedFrom.id ?? id,
|
|
470
|
+
expiresAt: body.rotatedFrom.expiresAt,
|
|
471
|
+
},
|
|
472
|
+
client: () => childClient(body.token),
|
|
473
|
+
};
|
|
474
|
+
},
|
|
442
475
|
mint(options) {
|
|
443
476
|
return capabilities.create(options);
|
|
444
477
|
},
|
|
@@ -533,14 +566,39 @@ export function createProtocolClient(options) {
|
|
|
533
566
|
return waitForNoIntents(target, options);
|
|
534
567
|
},
|
|
535
568
|
};
|
|
536
|
-
async function
|
|
537
|
-
|
|
538
|
-
|
|
569
|
+
async function listModel(modelName, options) {
|
|
570
|
+
const params = new URLSearchParams();
|
|
571
|
+
if (options?.limit !== undefined)
|
|
572
|
+
params.set('limit', String(options.limit));
|
|
573
|
+
if (options?.orderBy) {
|
|
574
|
+
const [col, dir] = Object.entries(options.orderBy)[0] ?? [];
|
|
575
|
+
if (col) {
|
|
576
|
+
params.set('order_by', col);
|
|
577
|
+
if (dir === 'desc')
|
|
578
|
+
params.set('order', 'desc');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// The collection route turns any non-reserved query param into an equality
|
|
582
|
+
// filter (`?status=todo`). The wire is AND-only equality — matches what a
|
|
583
|
+
// stateless reactor needs; richer predicates stay on the stateful path.
|
|
584
|
+
if (options?.where && typeof options.where === 'object') {
|
|
585
|
+
for (const [k, v] of Object.entries(options.where)) {
|
|
586
|
+
if (v !== undefined && v !== null && typeof v !== 'object')
|
|
587
|
+
params.set(k, String(v));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const qs = params.toString();
|
|
591
|
+
const res = await requestJson(`/v1/models/${encodeURIComponent(modelName)}${qs ? `?${qs}` : ''}`, { method: 'GET' });
|
|
592
|
+
return res.data ?? [];
|
|
593
|
+
}
|
|
594
|
+
async function retrieveModel(modelName, params) {
|
|
595
|
+
await applyClaimedPolicy({ model: modelName, id: params.id }, params);
|
|
596
|
+
const query = await requestJson(`/v1/models/${encodeURIComponent(modelName)}/${encodeURIComponent(params.id)}`, {
|
|
539
597
|
method: 'GET',
|
|
540
598
|
});
|
|
541
599
|
const data = query.data;
|
|
542
600
|
if (!data) {
|
|
543
|
-
throw new AbloValidationError(`Model row not found: ${modelName}/${id}`, { code: 'model_not_found' });
|
|
601
|
+
throw new AbloValidationError(`Model row not found: ${modelName}/${params.id}`, { code: 'model_not_found' });
|
|
544
602
|
}
|
|
545
603
|
return {
|
|
546
604
|
data,
|
|
@@ -548,63 +606,170 @@ export function createProtocolClient(options) {
|
|
|
548
606
|
claims: query.claims ?? [],
|
|
549
607
|
};
|
|
550
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* Single-op mutation over the model-scoped routes — the canonical surface
|
|
611
|
+
* that mirrors `ablo.<model>.create/update/delete`:
|
|
612
|
+
*
|
|
613
|
+
* POST /v1/models/:model create
|
|
614
|
+
* PATCH /v1/models/:model/:id update
|
|
615
|
+
* DELETE /v1/models/:model/:id delete
|
|
616
|
+
*
|
|
617
|
+
* This replaces the previous indirection through `POST /v1/commits`. The raw
|
|
618
|
+
* `commits.create(...)` resource is still the path for ATOMIC MULTI-OP
|
|
619
|
+
* envelopes — this helper is the one-op, one-record path only.
|
|
620
|
+
*/
|
|
621
|
+
async function mutateModel(action, modelName, id, data, options) {
|
|
622
|
+
const clientTxId = createClientTxId(options?.idempotencyKey);
|
|
623
|
+
const encModel = encodeURIComponent(modelName);
|
|
624
|
+
const path = action === 'create'
|
|
625
|
+
? `/v1/models/${encModel}`
|
|
626
|
+
: `/v1/models/${encModel}/${encodeURIComponent(id)}`;
|
|
627
|
+
const method = action === 'create' ? 'POST' : action === 'update' ? 'PATCH' : 'DELETE';
|
|
628
|
+
const requestBody = {
|
|
629
|
+
idempotencyKey: clientTxId,
|
|
630
|
+
intent: normalizeIntentId(options?.intent),
|
|
631
|
+
onStale: options?.onStale,
|
|
632
|
+
readAt: options?.readAt,
|
|
633
|
+
};
|
|
634
|
+
if (action === 'create')
|
|
635
|
+
requestBody.id = id;
|
|
636
|
+
if (data !== undefined)
|
|
637
|
+
requestBody.data = data;
|
|
638
|
+
const body = await requestJson(path, {
|
|
639
|
+
method,
|
|
640
|
+
idempotencyKey: clientTxId,
|
|
641
|
+
body: JSON.stringify(requestBody),
|
|
642
|
+
});
|
|
643
|
+
// `requestJson` throws via `translateHttpError` on any non-2xx, so reaching
|
|
644
|
+
// here implies success. Narrow `status` to the `CommitWait`-compatible
|
|
645
|
+
// subset; `'rejected'` only appears on a thrown rejection body.
|
|
646
|
+
const status = body.status === 'queued' ? 'queued' : 'confirmed';
|
|
647
|
+
return {
|
|
648
|
+
id: body.serverTxId ?? body.id ?? body.clientTxId ?? clientTxId,
|
|
649
|
+
status,
|
|
650
|
+
lastSyncId: body.lastSyncId,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
551
653
|
function model(name) {
|
|
654
|
+
// Durable lease + FIFO wait-line over HTTP (the existing claim routes). A
|
|
655
|
+
// claim is server state, not a subscription — acquire/hold/release are plain
|
|
656
|
+
// request/response, so a stateless agent participates in coordination too.
|
|
657
|
+
const claimPath = (id) => `/v1/models/${encodeURIComponent(name)}/${encodeURIComponent(id)}/claim`;
|
|
658
|
+
const isClaimHandle = (value) => typeof value === 'object' &&
|
|
659
|
+
value !== null &&
|
|
660
|
+
value.object === 'claim' &&
|
|
661
|
+
typeof value.claimId === 'string' &&
|
|
662
|
+
typeof value.release === 'function';
|
|
663
|
+
const claimMeta = (options) => {
|
|
664
|
+
if (!options?.description)
|
|
665
|
+
return options?.meta;
|
|
666
|
+
return { ...(options.meta ?? {}), description: options.description };
|
|
667
|
+
};
|
|
668
|
+
const acquireClaim = async (params) => {
|
|
669
|
+
const body = await requestJson(claimPath(params.id), {
|
|
670
|
+
method: 'POST',
|
|
671
|
+
body: JSON.stringify({
|
|
672
|
+
action: params.action ?? 'editing',
|
|
673
|
+
...(params.ttl !== undefined ? { ttl: params.ttl } : {}),
|
|
674
|
+
...(params.description !== undefined ? { description: params.description } : {}),
|
|
675
|
+
...(claimMeta(params) ? { meta: claimMeta(params) } : {}),
|
|
676
|
+
// `wait` (default true) → queue behind the holder; false → fail-fast
|
|
677
|
+
// with AbloClaimedError (work-distribution dedup).
|
|
678
|
+
queue: params.wait ?? true,
|
|
679
|
+
}),
|
|
680
|
+
});
|
|
681
|
+
if (body.status === 'queued') {
|
|
682
|
+
throw new AbloClaimedError(`Target ${name}/${params.id} is held; queued at position ${body.position ?? 0}. ` +
|
|
683
|
+
`The HTTP client cannot await the grant without a WebSocket.`, { code: 'intent_queued' });
|
|
684
|
+
}
|
|
685
|
+
return body.intent?.id ?? body.id ?? body.intentId ?? createIntentId();
|
|
686
|
+
};
|
|
687
|
+
const releaseClaim = (params) => requestJson(claimPath(isClaimHandle(params) ? params.target.id : params.id), { method: 'DELETE' }).then(() => undefined);
|
|
688
|
+
async function claimImpl(params) {
|
|
689
|
+
const claimId = await acquireClaim(params);
|
|
690
|
+
const { data } = await retrieveModel(name, { id: params.id });
|
|
691
|
+
const release = () => releaseClaim(params);
|
|
692
|
+
return {
|
|
693
|
+
object: 'claim',
|
|
694
|
+
claimId,
|
|
695
|
+
target: {
|
|
696
|
+
model: name,
|
|
697
|
+
id: params.id,
|
|
698
|
+
...(params.field ? { field: params.field } : {}),
|
|
699
|
+
...(params.path ? { path: params.path } : {}),
|
|
700
|
+
...(params.range ? { range: params.range } : {}),
|
|
701
|
+
...(claimMeta(params) ? { meta: claimMeta(params) } : {}),
|
|
702
|
+
},
|
|
703
|
+
action: params.action ?? 'editing',
|
|
704
|
+
...(params.description ? { description: params.description } : {}),
|
|
705
|
+
data,
|
|
706
|
+
release,
|
|
707
|
+
revoke: () => {
|
|
708
|
+
void release().catch(() => { });
|
|
709
|
+
},
|
|
710
|
+
[Symbol.asyncDispose]: release,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const intentsForEntity = async (params) => requestJson(`/v1/intents?model=${encodeURIComponent(name)}&id=${encodeURIComponent(params.id)}${params.field ? `&field=${encodeURIComponent(params.field)}` : ''}`, { method: 'GET' });
|
|
714
|
+
const claim = Object.assign(claimImpl, {
|
|
715
|
+
release: releaseClaim,
|
|
716
|
+
state: async (params) => {
|
|
717
|
+
const res = await intentsForEntity(params);
|
|
718
|
+
return res.intents?.[0] ?? null;
|
|
719
|
+
},
|
|
720
|
+
queue: async (params) => {
|
|
721
|
+
const res = await intentsForEntity(params);
|
|
722
|
+
return { object: 'list', data: res.queue ?? [] };
|
|
723
|
+
},
|
|
724
|
+
reorder: async (params) => {
|
|
725
|
+
await requestJson(`${claimPath(params.id)}/reorder`, {
|
|
726
|
+
method: 'POST',
|
|
727
|
+
// The reorder route's payload is `{ heldBy, intentId }[]` — Intent's id
|
|
728
|
+
// IS the intentId.
|
|
729
|
+
body: JSON.stringify({ order: params.order.map((i) => ({ heldBy: i.heldBy, intentId: i.id })) }),
|
|
730
|
+
});
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
const withMutationClaim = async (id, input, run) => {
|
|
734
|
+
const claimInput = input?.claim;
|
|
735
|
+
if (!claimInput)
|
|
736
|
+
return run(input);
|
|
737
|
+
if (isClaimHandle(claimInput)) {
|
|
738
|
+
return run({ ...input, intent: { id: claimInput.claimId }, claim: undefined });
|
|
739
|
+
}
|
|
740
|
+
const claimId = await acquireClaim({ id, ...claimInput });
|
|
741
|
+
try {
|
|
742
|
+
return await run({ ...input, intent: { id: claimId }, claim: undefined });
|
|
743
|
+
}
|
|
744
|
+
finally {
|
|
745
|
+
await releaseClaim({ id }).catch(() => { });
|
|
746
|
+
}
|
|
747
|
+
};
|
|
552
748
|
return {
|
|
553
|
-
|
|
554
|
-
|
|
749
|
+
claim,
|
|
750
|
+
retrieve(params) {
|
|
751
|
+
return retrieveModel(name, params);
|
|
752
|
+
},
|
|
753
|
+
list(options) {
|
|
754
|
+
return listModel(name, options);
|
|
555
755
|
},
|
|
556
|
-
async create(
|
|
557
|
-
const id =
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
],
|
|
756
|
+
async create(params) {
|
|
757
|
+
const id = params.id ?? createModelId();
|
|
758
|
+
return withMutationClaim(id, params, async (options) => {
|
|
759
|
+
await applyClaimedPolicy({ model: name, id }, options);
|
|
760
|
+
return mutateModel('create', name, id, params.data, options);
|
|
573
761
|
});
|
|
574
762
|
},
|
|
575
|
-
async update(
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
],
|
|
763
|
+
async update(params) {
|
|
764
|
+
return withMutationClaim(params.id, params, async (options) => {
|
|
765
|
+
await applyClaimedPolicy({ model: name, id: params.id }, options);
|
|
766
|
+
return mutateModel('update', name, params.id, params.data, options);
|
|
591
767
|
});
|
|
592
768
|
},
|
|
593
|
-
async delete(
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
],
|
|
769
|
+
async delete(params) {
|
|
770
|
+
return withMutationClaim(params.id, params, async (options) => {
|
|
771
|
+
await applyClaimedPolicy({ model: name, id: params.id }, options);
|
|
772
|
+
return mutateModel('delete', name, params.id, undefined, options);
|
|
608
773
|
});
|
|
609
774
|
},
|
|
610
775
|
};
|
|
@@ -620,6 +785,11 @@ export function createProtocolClient(options) {
|
|
|
620
785
|
commits,
|
|
621
786
|
model,
|
|
622
787
|
agent: createAgent,
|
|
788
|
+
async getAuthToken() {
|
|
789
|
+
// Mirror `authHeaders()`: a configured API key wins, else the
|
|
790
|
+
// construction-time auth token. Resolve the (possibly async) key setter.
|
|
791
|
+
return (await resolveApiKeyValue(configuredApiKey)) ?? configuredAuthToken ?? null;
|
|
792
|
+
},
|
|
623
793
|
async beginTurn(turnOptions) {
|
|
624
794
|
const task = await tasks.create(turnOptions);
|
|
625
795
|
let closed = false;
|
package/dist/client/auth.d.ts
CHANGED
|
@@ -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,34 @@ 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;
|
|
44
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the direct-URL connector's Postgres connection string.
|
|
47
|
+
*
|
|
48
|
+
* The default Data Source path should not call this: the customer keeps
|
|
49
|
+
* `DATABASE_URL` in their app and exposes `dataSource(...)`. This helper exists
|
|
50
|
+
* only for the opt-in direct connector where Ablo registers a dedicated tenant
|
|
51
|
+
* database. Returns null for Ablo-managed storage.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveDatabaseUrl(input: AuthResolveInput): string | null;
|
|
54
|
+
export declare const ABLO_HOSTED_API_DOMAIN = "api.abloatai.com";
|
|
55
|
+
export declare const ABLO_HOSTED_HTTP_BASE_URL = "https://api.abloatai.com";
|
|
56
|
+
export declare const ABLO_DEFAULT_BASE_URL = "wss://api.abloatai.com";
|
|
57
|
+
/**
|
|
58
|
+
* Normalize old hosted aliases to the public API domain. Self-hosted/custom
|
|
59
|
+
* URLs pass through unchanged; only first-party legacy hosts are rewritten.
|
|
60
|
+
*/
|
|
61
|
+
export declare function normalizeAbloHostedBaseUrl(rawUrl: string): string;
|
|
45
62
|
export declare function resolveBaseURL(input: AuthResolveInput): string;
|
|
46
63
|
/**
|
|
47
64
|
* Browser guard — apiKey is server-side-only by default. Same check
|
|
48
65
|
* Anthropic, OpenAI, and Stripe ship: shipping `sk_live_...` to a
|
|
49
66
|
* browser exposes it in every visitor's network tab. Consumers opt
|
|
50
|
-
* in explicitly when
|
|
67
|
+
* in explicitly when the browser holds a minted session token
|
|
68
|
+
* (`ek_`/`rk_`) or routes through a server proxy.
|
|
51
69
|
*/
|
|
52
70
|
export declare function assertBrowserSafety(input: {
|
|
53
71
|
apiKey: string | ApiKeySetter | null;
|
|
72
|
+
databaseUrl?: string | null;
|
|
54
73
|
dangerouslyAllowBrowser: boolean | undefined;
|
|
55
74
|
}): void;
|
|
56
75
|
/**
|
package/dist/client/auth.js
CHANGED
|
@@ -27,19 +27,71 @@ export function resolveApiKey(input) {
|
|
|
27
27
|
export function resolveAuthToken(input) {
|
|
28
28
|
return input.options.authToken ?? null;
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the direct-URL connector's Postgres connection string.
|
|
32
|
+
*
|
|
33
|
+
* The default Data Source path should not call this: the customer keeps
|
|
34
|
+
* `DATABASE_URL` in their app and exposes `dataSource(...)`. This helper exists
|
|
35
|
+
* only for the opt-in direct connector where Ablo registers a dedicated tenant
|
|
36
|
+
* database. Returns null for Ablo-managed storage.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveDatabaseUrl(input) {
|
|
39
|
+
return input.options.databaseUrl ?? input.env.DATABASE_URL ?? null;
|
|
40
|
+
}
|
|
41
|
+
export const ABLO_HOSTED_API_DOMAIN = 'api.abloatai.com';
|
|
42
|
+
export const ABLO_HOSTED_HTTP_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
|
|
43
|
+
export const ABLO_DEFAULT_BASE_URL = `wss://${ABLO_HOSTED_API_DOMAIN}`;
|
|
44
|
+
const LEGACY_HOSTED_API_HOSTS = new Set([
|
|
45
|
+
'mesh.ablo.finance',
|
|
46
|
+
'mesh-staging.ablo.finance',
|
|
47
|
+
'api.ablo.finance',
|
|
48
|
+
'sync-staging.ablo.finance',
|
|
49
|
+
]);
|
|
50
|
+
/**
|
|
51
|
+
* Normalize old hosted aliases to the public API domain. Self-hosted/custom
|
|
52
|
+
* URLs pass through unchanged; only first-party legacy hosts are rewritten.
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeAbloHostedBaseUrl(rawUrl) {
|
|
55
|
+
const trimmed = rawUrl.trim();
|
|
56
|
+
if (!trimmed)
|
|
57
|
+
return trimmed;
|
|
58
|
+
// A scheme-less value (e.g. `api-staging.abloatai.com`) is a RELATIVE URL:
|
|
59
|
+
// `new URL()` throws on it, and downstream `fetch` then resolves it against
|
|
60
|
+
// the current page — producing `https://<app-host>/<route>/api-staging…/api/
|
|
61
|
+
// auth/identity`, a 404 from the app's own origin. Prepend a scheme so the
|
|
62
|
+
// base is absolute. `https` mirrors `ABLO_HOSTED_HTTP_BASE_URL`; the socket
|
|
63
|
+
// layer derives `wss` from it. An existing scheme (ws/wss/http/https) is
|
|
64
|
+
// preserved untouched.
|
|
65
|
+
const schemed = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
66
|
+
try {
|
|
67
|
+
const url = new URL(schemed);
|
|
68
|
+
if (!LEGACY_HOSTED_API_HOSTS.has(url.hostname))
|
|
69
|
+
return schemed.replace(/\/+$/, '');
|
|
70
|
+
url.hostname = ABLO_HOSTED_API_DOMAIN;
|
|
71
|
+
if (url.protocol === 'http:')
|
|
72
|
+
url.protocol = 'https:';
|
|
73
|
+
if (url.protocol === 'ws:')
|
|
74
|
+
url.protocol = 'wss:';
|
|
75
|
+
return url.toString().replace(/\/+$/, '');
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return schemed;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
31
81
|
export function resolveBaseURL(input) {
|
|
32
|
-
return input.options.baseURL ?? ABLO_DEFAULT_BASE_URL;
|
|
82
|
+
return normalizeAbloHostedBaseUrl(input.options.baseURL ?? ABLO_DEFAULT_BASE_URL);
|
|
33
83
|
}
|
|
34
84
|
/**
|
|
35
85
|
* Browser guard — apiKey is server-side-only by default. Same check
|
|
36
86
|
* Anthropic, OpenAI, and Stripe ship: shipping `sk_live_...` to a
|
|
37
87
|
* browser exposes it in every visitor's network tab. Consumers opt
|
|
38
|
-
* in explicitly when
|
|
88
|
+
* in explicitly when the browser holds a minted session token
|
|
89
|
+
* (`ek_`/`rk_`) or routes through a server proxy.
|
|
39
90
|
*/
|
|
40
91
|
export function assertBrowserSafety(input) {
|
|
92
|
+
const inBrowser = typeof window !== 'undefined';
|
|
41
93
|
if (!input.dangerouslyAllowBrowser &&
|
|
42
|
-
|
|
94
|
+
inBrowser &&
|
|
43
95
|
typeof input.apiKey === 'string' &&
|
|
44
96
|
input.apiKey.startsWith('sk_')) {
|
|
45
97
|
throw new AbloAuthenticationError("It looks like you're running in a browser-like environment.\n\n" +
|
|
@@ -49,6 +101,14 @@ export function assertBrowserSafety(input) {
|
|
|
49
101
|
'`dangerouslyAllowBrowser` option to `true`, e.g.,\n\n' +
|
|
50
102
|
' Ablo({ schema, apiKey, dangerouslyAllowBrowser: true });\n', { code: 'browser_apikey_blocked' });
|
|
51
103
|
}
|
|
104
|
+
// `databaseUrl` carries DB credentials and is NEVER browser-safe, so
|
|
105
|
+
// `dangerouslyAllowBrowser` does not override it. Register your database from
|
|
106
|
+
// a server-side runtime.
|
|
107
|
+
if (inBrowser && typeof input.databaseUrl === 'string' && input.databaseUrl.length > 0) {
|
|
108
|
+
throw new AbloAuthenticationError('Ablo `databaseUrl` cannot be used in a browser-like environment — it ' +
|
|
109
|
+
'carries your database credentials. Initialize the client with ' +
|
|
110
|
+
'`databaseUrl` from a server-side runtime only.', { code: 'browser_database_url_blocked' });
|
|
111
|
+
}
|
|
52
112
|
}
|
|
53
113
|
/**
|
|
54
114
|
* Resolve an `ApiKeySetter` callable to its current string value.
|
|
@@ -77,5 +137,17 @@ export async function resolveApiKeyValue(apiKey) {
|
|
|
77
137
|
* preserves the protocol family (ws → http, wss → https).
|
|
78
138
|
*/
|
|
79
139
|
export function resolveBootstrapBaseUrl(input) {
|
|
80
|
-
|
|
140
|
+
if (input.bootstrapBaseUrl) {
|
|
141
|
+
// Coerce ws/wss → http/https on the override path too. This base URL is
|
|
142
|
+
// used for HTTP fetches (identity resolve, apiKey exchange, bootstrap) and
|
|
143
|
+
// the browser `fetch` rejects ws/wss schemes outright ("URL scheme \"wss\"
|
|
144
|
+
// is not supported"). apps/web derives this override as `${baseUrl}/api`
|
|
145
|
+
// where `baseUrl` may carry a WebSocket scheme, so the override can
|
|
146
|
+
// legitimately arrive as `wss://…` — normalize it here rather than
|
|
147
|
+
// faceplanting at fetch time. The derive branch below already does this;
|
|
148
|
+
// the override branch silently skipped it.
|
|
149
|
+
return normalizeAbloHostedBaseUrl(input.bootstrapBaseUrl).replace(/^ws/, 'http');
|
|
150
|
+
}
|
|
151
|
+
const url = normalizeAbloHostedBaseUrl(input.url);
|
|
152
|
+
return `${url.replace(/^ws/, 'http')}/api`;
|
|
81
153
|
}
|
|
@@ -16,6 +16,7 @@ import { ObjectPool } from '../ObjectPool.js';
|
|
|
16
16
|
import { SyncClient } from '../SyncClient.js';
|
|
17
17
|
import { HydrationCoordinator } from '../sync/HydrationCoordinator.js';
|
|
18
18
|
import { BootstrapHelper } from '../sync/BootstrapHelper.js';
|
|
19
|
+
import type { AuthCredentialSource } from '../auth/credentialSource.js';
|
|
19
20
|
import type { Schema, SchemaRecord } from '../schema/schema.js';
|
|
20
21
|
import { type AbloPersistence } from './persistence.js';
|
|
21
22
|
export interface InternalComponentsInput<S extends SchemaRecord> {
|
|
@@ -31,6 +32,7 @@ export interface InternalComponentsInput<S extends SchemaRecord> {
|
|
|
31
32
|
readonly offline?: boolean;
|
|
32
33
|
readonly inMemory?: boolean;
|
|
33
34
|
};
|
|
35
|
+
readonly auth?: AuthCredentialSource;
|
|
34
36
|
}
|
|
35
37
|
export interface InternalComponents {
|
|
36
38
|
readonly modelRegistry: ModelRegistry;
|
|
@@ -19,7 +19,7 @@ import { BootstrapHelper } from '../sync/BootstrapHelper.js';
|
|
|
19
19
|
import { resolveBootstrapBaseUrl } from './auth.js';
|
|
20
20
|
import { shouldUseInMemoryPersistence } from './persistence.js';
|
|
21
21
|
export function createInternalComponents(input) {
|
|
22
|
-
const { schema, url, options } = input;
|
|
22
|
+
const { schema, url, options, auth } = input;
|
|
23
23
|
// The registry is created here but model registration happens in
|
|
24
24
|
// the caller (Ablo.ts owns `registerModelsFromSchema` since the
|
|
25
25
|
// schema-to-class translation depends on private helpers there).
|
|
@@ -37,6 +37,7 @@ export function createInternalComponents(input) {
|
|
|
37
37
|
baseUrl: bootstrapBaseUrl,
|
|
38
38
|
syncGroups: options.syncGroups,
|
|
39
39
|
instantModels: deriveInstantModels(schema),
|
|
40
|
+
getAuthToken: auth?.getAuthToken,
|
|
40
41
|
});
|
|
41
42
|
const database = new Database(modelRegistry, bootstrapHelper, {
|
|
42
43
|
// Point-solution default: no browser-local durable store unless the
|
|
@@ -55,7 +56,13 @@ export function createInternalComponents(input) {
|
|
|
55
56
|
registry: modelRegistry,
|
|
56
57
|
schema,
|
|
57
58
|
baseUrl: bootstrapBaseUrl,
|
|
59
|
+
getAuthToken: auth?.getAuthToken,
|
|
58
60
|
});
|
|
61
|
+
// Drop the lazy-lane hydration ledger on reconnect. While connected, the
|
|
62
|
+
// WebSocket delta stream keeps hydrated rows fresh so repeat reads serve
|
|
63
|
+
// pure-local with no network; after a drop, deltas may have been missed, so
|
|
64
|
+
// the next read of each query must re-confirm with the server once.
|
|
65
|
+
syncClient.on('sync:reconnecting', () => hydration.invalidate());
|
|
59
66
|
return {
|
|
60
67
|
modelRegistry,
|
|
61
68
|
objectPool,
|