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