@abloatai/ablo 0.11.1 → 0.12.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 +49 -0
- package/README.md +10 -2
- package/dist/Model.d.ts +39 -0
- package/dist/Model.js +68 -0
- package/dist/ai-sdk/claim-broadcast.d.ts +4 -3
- package/dist/ai-sdk/claim-broadcast.js +2 -2
- package/dist/ai-sdk/wrap.d.ts +5 -4
- package/dist/ai-sdk/wrap.js +3 -3
- package/dist/auth/credentialPolicy.d.ts +145 -0
- package/dist/auth/credentialPolicy.js +130 -0
- package/dist/cli.cjs +42 -7
- package/dist/client/Ablo.d.ts +64 -91
- package/dist/client/Ablo.js +43 -103
- package/dist/client/ApiClient.d.ts +10 -1
- package/dist/client/ApiClient.js +45 -22
- package/dist/client/auth.d.ts +12 -5
- package/dist/client/auth.js +2 -1
- package/dist/client/createModelProxy.d.ts +64 -17
- package/dist/client/createModelProxy.js +18 -12
- package/dist/client/httpClient.d.ts +17 -3
- package/dist/client/httpClient.js +1 -0
- package/dist/client/identity.js +134 -122
- package/dist/client/index.d.ts +1 -1
- package/dist/client/sessionMint.d.ts +15 -0
- package/dist/client/sessionMint.js +86 -0
- package/dist/coordination/schema.d.ts +1 -1
- package/dist/coordination/schema.js +3 -1
- package/dist/errorCodes.d.ts +2 -0
- package/dist/errorCodes.js +2 -0
- package/dist/errors.d.ts +6 -3
- package/dist/errors.js +9 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -7
- package/dist/mutators/RecordingTransaction.js +14 -42
- package/dist/react/AbloProvider.d.ts +12 -13
- package/dist/react/AbloProvider.js +10 -10
- package/dist/react/context.d.ts +10 -45
- package/dist/react/context.js +12 -17
- package/dist/react/index.d.ts +8 -10
- package/dist/react/index.js +8 -11
- package/dist/react/useMutators.js +3 -2
- package/dist/react/useSyncStatus.d.ts +1 -1
- package/dist/react/useUndoScope.js +3 -2
- package/dist/realtime/index.d.ts +1 -1
- package/dist/schema/generate.js +1 -2
- package/dist/schema/model.d.ts +10 -3
- package/dist/schema/schema.d.ts +13 -2
- package/dist/schema/schema.js +26 -0
- package/dist/surface.d.ts +29 -0
- package/dist/surface.js +60 -0
- package/dist/sync/ConnectionManager.d.ts +16 -5
- package/dist/sync/ConnectionManager.js +42 -7
- package/dist/sync/createClaimStream.js +5 -4
- package/dist/sync/participants.js +1 -1
- package/dist/transactions/TransactionQueue.d.ts +0 -11
- package/dist/transactions/TransactionQueue.js +12 -56
- package/dist/types/global.d.ts +3 -0
- package/dist/types/streams.d.ts +17 -29
- package/dist/utils/mobx-setup.js +1 -0
- package/docs/api-keys.md +49 -0
- package/docs/api.md +3 -2
- package/docs/client-behavior.md +1 -0
- package/docs/coordination.md +75 -21
- package/docs/examples/existing-python-backend.md +9 -5
- package/docs/examples/scoped-agent.md +1 -1
- package/docs/guarantees.md +4 -3
- package/docs/identity.md +89 -82
- package/docs/integration-guide.md +19 -10
- package/docs/migration.md +11 -3
- package/docs/quickstart.md +6 -2
- package/docs/react.md +3 -3
- package/docs/schema-contract.md +23 -5
- package/llms-full.txt +18 -16
- package/llms.txt +6 -6
- package/package.json +1 -1
- package/dist/api/index.d.ts +0 -10
- package/dist/api/index.js +0 -9
- package/dist/principal.d.ts +0 -44
- package/dist/principal.js +0 -49
- package/dist/react/SyncGroupProvider.d.ts +0 -19
- package/dist/react/SyncGroupProvider.js +0 -44
- package/dist/react/useClaim.d.ts +0 -29
- package/dist/react/useClaim.js +0 -42
- package/dist/react/usePresence.d.ts +0 -32
- package/dist/react/usePresence.js +0 -41
package/dist/client/Ablo.js
CHANGED
|
@@ -27,7 +27,7 @@ import { initSyncEngine } from '../context.js';
|
|
|
27
27
|
import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../SyncEngineContext.js';
|
|
28
28
|
import { alwaysOnline } from '../adapters/alwaysOnline.js';
|
|
29
29
|
import { validateAbloOptions } from './validateAbloOptions.js';
|
|
30
|
-
import {
|
|
30
|
+
import { mintSession } from './sessionMint.js';
|
|
31
31
|
import { createAuthCredentialSource } from '../auth/credentialSource.js';
|
|
32
32
|
import { createInternalComponents } from './createInternalComponents.js';
|
|
33
33
|
import { resolveParticipantIdentity } from './identity.js';
|
|
@@ -670,32 +670,20 @@ function createDefaultMutationDispatcher(executor) {
|
|
|
670
670
|
// ── Auth normalization ─────────────────────────────────────────────────────
|
|
671
671
|
/**
|
|
672
672
|
* The one resolver the credential lifecycle needs: an async `() => token | null`,
|
|
673
|
-
* or `null` when auth is static (a plain long-lived `apiKey` with no
|
|
674
|
-
* the common case).
|
|
675
|
-
*
|
|
673
|
+
* or `null` when auth is static (a plain long-lived `apiKey` STRING with no
|
|
674
|
+
* refresh — the common case).
|
|
675
|
+
*
|
|
676
|
+
* The short-lived per-user browser path passes a FUNCTION `apiKey` (an
|
|
677
|
+
* `ApiKeySetter`): the SDK then drives the full credential lifecycle off it —
|
|
678
|
+
* mint-before-connect, the proactive refresh timer + wake/online/focus re-mint,
|
|
679
|
+
* and the reactive `credential_stale` re-mint. The resolver's contract is the
|
|
680
|
+
* `ApiKeySetter` contract end-to-end: resolve a token, resolve `null` when the
|
|
681
|
+
* login is gone (terminal → `session_expired` → sign out), or THROW on a
|
|
682
|
+
* transient failure (→ back off, never sign out).
|
|
676
683
|
*/
|
|
677
|
-
function resolveCredentialResolver(
|
|
678
|
-
if (
|
|
679
|
-
return
|
|
680
|
-
if (options.authEndpoint) {
|
|
681
|
-
const endpoint = options.authEndpoint;
|
|
682
|
-
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
683
|
-
return async () => {
|
|
684
|
-
// The endpoint lives on the consumer's OWN backend and is authed by the
|
|
685
|
-
// user's session cookie (hence `credentials: 'include'`); it returns the
|
|
686
|
-
// `ek_` to carry to the sync-server. A non-OK response is terminal
|
|
687
|
-
// (`null` → sign out), matching the `getToken` contract.
|
|
688
|
-
const res = await fetchImpl(endpoint, {
|
|
689
|
-
method: 'POST',
|
|
690
|
-
credentials: 'include',
|
|
691
|
-
headers: { 'Content-Type': 'application/json' },
|
|
692
|
-
});
|
|
693
|
-
if (!res.ok)
|
|
694
|
-
return null;
|
|
695
|
-
const body = (await res.json());
|
|
696
|
-
return body.token ?? null;
|
|
697
|
-
};
|
|
698
|
-
}
|
|
684
|
+
function resolveCredentialResolver(apiKey) {
|
|
685
|
+
if (typeof apiKey === 'function')
|
|
686
|
+
return apiKey;
|
|
699
687
|
return null;
|
|
700
688
|
}
|
|
701
689
|
export function Ablo(options) {
|
|
@@ -716,7 +704,7 @@ export function Ablo(options) {
|
|
|
716
704
|
// drives both the reactive re-mint (FSM `credential_stale`) and the proactive
|
|
717
705
|
// refresh timer + wake/online/focus triggers. Null for the common static
|
|
718
706
|
// `apiKey` path — no refresh needed.
|
|
719
|
-
const credentialResolver = resolveCredentialResolver(
|
|
707
|
+
const credentialResolver = resolveCredentialResolver(configuredApiKey);
|
|
720
708
|
const authCredentials = createAuthCredentialSource(internalOptions.capabilityToken ?? configuredAuthToken);
|
|
721
709
|
const configuredDatabaseUrl = resolveDatabaseUrl(authInput);
|
|
722
710
|
assertBrowserSafety({
|
|
@@ -900,7 +888,7 @@ export function Ablo(options) {
|
|
|
900
888
|
// WebSocket upgrade + bootstrap carry a valid bearer (no tokenless first
|
|
901
889
|
// connect that has to self-heal). Only when a refreshing resolver is
|
|
902
890
|
// wired AND no static credential is already present. Contract mirrors
|
|
903
|
-
// `
|
|
891
|
+
// the `apiKey` resolver: `null` ⇒ the login is gone (terminal — fail ready so the
|
|
904
892
|
// app shows sign-in); a THROW ⇒ transient (rethrown; autoStart swallows
|
|
905
893
|
// and the lifecycle's online/wake triggers retry).
|
|
906
894
|
if (credentialResolver && !authCredentials.getAuthToken()) {
|
|
@@ -933,8 +921,9 @@ export function Ablo(options) {
|
|
|
933
921
|
kind,
|
|
934
922
|
configuredApiKey,
|
|
935
923
|
// Resolve identity against the LIVE token, not the construction-time
|
|
936
|
-
// `configuredAuthToken`. Consumers using `
|
|
937
|
-
// pass `authToken` at construction —
|
|
924
|
+
// `configuredAuthToken`. Consumers using a function `apiKey` (apps/web)
|
|
925
|
+
// never pass `authToken` at construction — the lifecycle mints the
|
|
926
|
+
// first `ek_`/`rk_` and calls `setAuthToken()` before
|
|
938
927
|
// `ready()`, which updates the shared credential source. Reading the frozen
|
|
939
928
|
// `configuredAuthToken` here made `/auth/identity` fire with no Bearer
|
|
940
929
|
// (→ `no_matching_provider` / `session_expired`) even though the JWT
|
|
@@ -1135,7 +1124,7 @@ export function Ablo(options) {
|
|
|
1135
1124
|
id: claim.id,
|
|
1136
1125
|
actor: claim.heldBy,
|
|
1137
1126
|
participantKind: claim.participantKind,
|
|
1138
|
-
|
|
1127
|
+
reason: claim.reason,
|
|
1139
1128
|
...(description ? { description } : {}),
|
|
1140
1129
|
field: claim.target.field,
|
|
1141
1130
|
status: 'active',
|
|
@@ -1155,7 +1144,7 @@ export function Ablo(options) {
|
|
|
1155
1144
|
id: claim.id,
|
|
1156
1145
|
actor: claim.heldBy,
|
|
1157
1146
|
participantKind: claim.participantKind,
|
|
1158
|
-
|
|
1147
|
+
reason: claim.reason,
|
|
1159
1148
|
...(claim.description ? { description: claim.description } : {}),
|
|
1160
1149
|
field: claim.target.field,
|
|
1161
1150
|
status: 'queued',
|
|
@@ -1247,14 +1236,8 @@ export function Ablo(options) {
|
|
|
1247
1236
|
const current = listModelClaims(target);
|
|
1248
1237
|
if (current.length === 0)
|
|
1249
1238
|
return;
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
const queue = listModelClaimQueue(target);
|
|
1253
|
-
if (options?.maxQueueDepth !== undefined &&
|
|
1254
|
-
queue.length >= options.maxQueueDepth) {
|
|
1255
|
-
throw claimedError(target, current, 'queue_too_deep');
|
|
1256
|
-
}
|
|
1257
|
-
await waitForModelUnclaimed(target, { timeout: options?.claimedTimeout });
|
|
1239
|
+
// policy === 'fail' — gate the read only when the caller opts in.
|
|
1240
|
+
throw claimedError(target, current, 'model_claimed');
|
|
1258
1241
|
}
|
|
1259
1242
|
function wrapClaimHandle(claim, waited = false) {
|
|
1260
1243
|
const release = async () => {
|
|
@@ -1263,7 +1246,7 @@ export function Ablo(options) {
|
|
|
1263
1246
|
return {
|
|
1264
1247
|
object: 'claim',
|
|
1265
1248
|
claimId: claim.claimId,
|
|
1266
|
-
|
|
1249
|
+
reason: claim.reason,
|
|
1267
1250
|
target: claim.target,
|
|
1268
1251
|
waited,
|
|
1269
1252
|
release,
|
|
@@ -1282,7 +1265,7 @@ export function Ablo(options) {
|
|
|
1282
1265
|
field: claimOptions.target.field,
|
|
1283
1266
|
meta: claimOptions.target.meta,
|
|
1284
1267
|
}, {
|
|
1285
|
-
reason: claimOptions.
|
|
1268
|
+
reason: claimOptions.reason,
|
|
1286
1269
|
ttl: claimOptions.ttl,
|
|
1287
1270
|
queue: claimOptions.queue,
|
|
1288
1271
|
});
|
|
@@ -1365,7 +1348,7 @@ export function Ablo(options) {
|
|
|
1365
1348
|
...(held.target.field ? { field: held.target.field } : {}),
|
|
1366
1349
|
...(held.target.meta ? { meta: held.target.meta } : {}),
|
|
1367
1350
|
},
|
|
1368
|
-
|
|
1351
|
+
reason: held.reason,
|
|
1369
1352
|
heldBy: held.actor,
|
|
1370
1353
|
participantKind: held.participantKind,
|
|
1371
1354
|
expiresAt: held.expiresAt,
|
|
@@ -1385,6 +1368,14 @@ export function Ablo(options) {
|
|
|
1385
1368
|
// errors so read interest never makes a read reject or stall.
|
|
1386
1369
|
enterScope: (scope) => store.enterScope(scope),
|
|
1387
1370
|
pinScope: (scope) => store.pinScope(scope),
|
|
1371
|
+
// `ablo.<model>.watch(ids, { ttl })` → a scoped participant join on
|
|
1372
|
+
// this model's sync group(s). The model-scoped relocation of the old
|
|
1373
|
+
// `ablo.participants.join({ scope: { <model>: ids } })`. WebSocket
|
|
1374
|
+
// only — `join` throws AbloConnectionError if the socket isn't ready.
|
|
1375
|
+
createWatch: (modelKey, ids, options) => participantManager.join({
|
|
1376
|
+
scope: { [modelKey]: ids },
|
|
1377
|
+
...(options?.ttl !== undefined ? { ttlSeconds: options.ttl } : {}),
|
|
1378
|
+
}),
|
|
1388
1379
|
});
|
|
1389
1380
|
}
|
|
1390
1381
|
const commits = {
|
|
@@ -1544,6 +1535,9 @@ export function Ablo(options) {
|
|
|
1544
1535
|
* (a wide-scope `rk_` on the hosted path), which control-plane routes
|
|
1545
1536
|
* rightly refuse (e.g. the user-session mint is sk_-gated). Counterpart to
|
|
1546
1537
|
* `getAuthToken()`, which resolves the sync-plane token.
|
|
1538
|
+
*
|
|
1539
|
+
* The sk_-only rule is enforced server-side; the credential KIND taxonomy
|
|
1540
|
+
* (secret/restricted/ephemeral/publishable) lives in `auth/credentialPolicy`.
|
|
1547
1541
|
*/
|
|
1548
1542
|
async function controlPlaneApiKey() {
|
|
1549
1543
|
return resolveApiKeyValue(configuredApiKey);
|
|
@@ -1564,7 +1558,7 @@ export function Ablo(options) {
|
|
|
1564
1558
|
store.nudgeReconnect();
|
|
1565
1559
|
},
|
|
1566
1560
|
async getAuthToken() {
|
|
1567
|
-
// The live short-lived bearer (set via `setAuthToken
|
|
1561
|
+
// The live short-lived bearer (set via `setAuthToken` / `apiKey`-resolver refresh)
|
|
1568
1562
|
// is the canonical credential; fall back to a configured API key.
|
|
1569
1563
|
//
|
|
1570
1564
|
// This is the SYNC-PLANE token (bootstrap, WS, query HTTP). Control-plane
|
|
@@ -1608,66 +1602,15 @@ export function Ablo(options) {
|
|
|
1608
1602
|
url,
|
|
1609
1603
|
bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
|
|
1610
1604
|
});
|
|
1611
|
-
//
|
|
1612
|
-
//
|
|
1613
|
-
//
|
|
1614
|
-
//
|
|
1615
|
-
|
|
1616
|
-
// (`invalid_participant_kind`, the 2026-06-11 Pulse
|
|
1617
|
-
// cascade: the SDK's own blessed pattern 403'd and
|
|
1618
|
-
// integrators fell back to minting humans as agents).
|
|
1619
|
-
// `{ agent }` → POST /auth/capability → scoped `rk_`.
|
|
1620
|
-
// `can: { tasks: ['update'] }` serializes to the wire
|
|
1621
|
-
// allowlist (`tasks.update`); the Hub matches it
|
|
1622
|
-
// against every registered alias of the model.
|
|
1623
|
-
if (params.user) {
|
|
1624
|
-
const res = await mintUserSessionKey({
|
|
1625
|
-
apiKey,
|
|
1626
|
-
baseUrl,
|
|
1627
|
-
userId: params.user.id,
|
|
1628
|
-
...(params.syncGroups ? { syncGroups: [...params.syncGroups] } : {}),
|
|
1629
|
-
ttlSeconds: params.ttlSeconds ?? 900,
|
|
1630
|
-
...(internalOptions.fetch ? { fetch: internalOptions.fetch } : {}),
|
|
1631
|
-
});
|
|
1632
|
-
return {
|
|
1633
|
-
object: 'session',
|
|
1634
|
-
id: res.id,
|
|
1635
|
-
token: res.token,
|
|
1636
|
-
expiresAt: res.expiresAt,
|
|
1637
|
-
organizationId: res.organizationId,
|
|
1638
|
-
// The ephemeral mint stores scope on the key row; reshape its flat
|
|
1639
|
-
// response into the session resource's scope block.
|
|
1640
|
-
scope: {
|
|
1641
|
-
organizationId: res.organizationId,
|
|
1642
|
-
syncGroups: res.syncGroups,
|
|
1643
|
-
operations: [],
|
|
1644
|
-
participantKind: 'user',
|
|
1645
|
-
participantId: res.participantId,
|
|
1646
|
-
},
|
|
1647
|
-
userMeta: params.userMeta ?? { id: res.participantId },
|
|
1648
|
-
};
|
|
1649
|
-
}
|
|
1650
|
-
const operations = Object.entries(params.can).flatMap(([model, ops]) => (ops ?? []).map((op) => `${model.toLowerCase()}.${op}`));
|
|
1651
|
-
const res = await exchangeApiKey({
|
|
1605
|
+
// The two mint doors (`{ user }` → /auth/ephemeral-keys → `ek_`,
|
|
1606
|
+
// `{ agent, can }` → /auth/capability → scoped `rk_`) live in the shared
|
|
1607
|
+
// `mintSession` so this stateful client and the stateless HTTP client can
|
|
1608
|
+
// never drift on how a token is minted.
|
|
1609
|
+
return mintSession(params, {
|
|
1652
1610
|
apiKey,
|
|
1653
1611
|
baseUrl,
|
|
1654
|
-
participantKind: 'agent',
|
|
1655
|
-
participantId: params.agent.id,
|
|
1656
|
-
...(params.syncGroups ? { syncGroups: [...params.syncGroups] } : {}),
|
|
1657
|
-
operations,
|
|
1658
|
-
ttlSeconds: params.ttlSeconds ?? 900,
|
|
1659
|
-
...(params.userMeta ? { userMeta: params.userMeta } : {}),
|
|
1660
1612
|
...(internalOptions.fetch ? { fetch: internalOptions.fetch } : {}),
|
|
1661
1613
|
});
|
|
1662
|
-
return {
|
|
1663
|
-
object: 'session',
|
|
1664
|
-
id: res.capabilityId,
|
|
1665
|
-
token: res.token,
|
|
1666
|
-
expiresAt: res.expiresAt,
|
|
1667
|
-
organizationId: res.organizationId,
|
|
1668
|
-
scope: res.scope,
|
|
1669
|
-
userMeta: res.userMeta,
|
|
1670
|
-
};
|
|
1671
1614
|
},
|
|
1672
1615
|
},
|
|
1673
1616
|
async dispose() {
|
|
@@ -1736,9 +1679,6 @@ export function Ablo(options) {
|
|
|
1736
1679
|
claims: publicClaims,
|
|
1737
1680
|
commits,
|
|
1738
1681
|
model,
|
|
1739
|
-
/** Structured multiplayer participation — target-first, no
|
|
1740
|
-
* sync-group strings in the common path. */
|
|
1741
|
-
participants: participantManager,
|
|
1742
1682
|
/** Context-staleness snapshot — see `engine.snapshot(...)` JSDoc. */
|
|
1743
1683
|
snapshot(entities) {
|
|
1744
1684
|
return createSnapshot({
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* IndexedDB, no WebSocket. It maps the public Model / Claim / Commit
|
|
6
6
|
* nouns directly to HTTP routes on sync-server.
|
|
7
7
|
*/
|
|
8
|
-
import type { AbloOptions, CommitResource, ClaimCreateOptions, ClaimWaitOptions, ModelClient, ModelClaim, ModelTarget } from './Ablo.js';
|
|
8
|
+
import type { AbloOptions, CommitResource, ClaimCreateOptions, ClaimWaitOptions, ModelClient, ModelClaim, ModelTarget, CreateSessionParams, AbloSession } from './Ablo.js';
|
|
9
|
+
import type { SchemaRecord } from '../schema/schema.js';
|
|
9
10
|
import type { ClaimHandle } from './createModelProxy.js';
|
|
10
11
|
import type { Duration } from '../utils/duration.js';
|
|
11
12
|
export type AbloApiClientOptions = Omit<AbloOptions, 'schema'> & {
|
|
@@ -133,5 +134,13 @@ export interface AbloApi {
|
|
|
133
134
|
* server with the credential this client already holds — no re-mint.
|
|
134
135
|
*/
|
|
135
136
|
getAuthToken(): Promise<string | null>;
|
|
137
|
+
/**
|
|
138
|
+
* Mint a short-lived scoped session — the Stripe `ephemeralKeys.create` shape.
|
|
139
|
+
* Minting is a control-plane HTTP call (no socket), so it lives on this stateless
|
|
140
|
+
* client too, not only the realtime one. `{ user }` → `ek_`, `{ agent, can }` → `rk_`.
|
|
141
|
+
*/
|
|
142
|
+
readonly sessions: {
|
|
143
|
+
create(params: CreateSessionParams<SchemaRecord>): Promise<AbloSession>;
|
|
144
|
+
};
|
|
136
145
|
}
|
|
137
146
|
export declare function createProtocolClient(options: AbloApiClientOptions): AbloApi;
|
package/dist/client/ApiClient.js
CHANGED
|
@@ -9,7 +9,20 @@ import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloVal
|
|
|
9
9
|
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfDatabaseUrlEnvIgnored, } from './auth.js';
|
|
10
10
|
import { registerDataSource } from './registerDataSource.js';
|
|
11
11
|
import { toSeconds } from '../utils/duration.js';
|
|
12
|
+
import { mintSession } from './sessionMint.js';
|
|
12
13
|
import { assertWriteOptions } from './writeOptionsSchema.js';
|
|
14
|
+
/**
|
|
15
|
+
* The `/v1/claims` and model-query routes still emit the wire field `action`
|
|
16
|
+
* for the claim phase; the public `Claim` / `ModelClaim` expose it as `reason`.
|
|
17
|
+
* Heal on read so the SDK shape is consistent without a coordinated server
|
|
18
|
+
* deploy — `reason ?? action`. When the server adopts `reason`, this is a no-op.
|
|
19
|
+
*/
|
|
20
|
+
function healClaimPhase(claim) {
|
|
21
|
+
const raw = claim;
|
|
22
|
+
if (raw.reason !== undefined)
|
|
23
|
+
return claim;
|
|
24
|
+
return { ...claim, reason: raw.action ?? 'editing' };
|
|
25
|
+
}
|
|
13
26
|
const DEFAULT_AGENT_LEASE = '10m';
|
|
14
27
|
export function createProtocolClient(options) {
|
|
15
28
|
const env = readProcessEnv();
|
|
@@ -172,8 +185,8 @@ export function createProtocolClient(options) {
|
|
|
172
185
|
const suffix = params.toString();
|
|
173
186
|
const body = await requestJson(`/v1/claims${suffix ? `?${suffix}` : ''}`, { method: 'GET' });
|
|
174
187
|
return {
|
|
175
|
-
active: body.claims ?? [],
|
|
176
|
-
queue: body.queue ?? [],
|
|
188
|
+
active: (body.claims ?? []).map(healClaimPhase),
|
|
189
|
+
queue: (body.queue ?? []).map(healClaimPhase),
|
|
177
190
|
};
|
|
178
191
|
}
|
|
179
192
|
function delay(ms, signal) {
|
|
@@ -228,20 +241,11 @@ export function createProtocolClient(options) {
|
|
|
228
241
|
const policy = options?.ifClaimed ?? defaultPolicy;
|
|
229
242
|
if (policy === 'return')
|
|
230
243
|
return;
|
|
244
|
+
// policy === 'fail' — gate the read only when the caller opts in.
|
|
231
245
|
const state = await listClaimState(target);
|
|
232
246
|
if (state.active.length === 0)
|
|
233
247
|
return;
|
|
234
|
-
|
|
235
|
-
throw claimedError(target, state.active, 'model_claimed');
|
|
236
|
-
}
|
|
237
|
-
if (options?.maxQueueDepth !== undefined &&
|
|
238
|
-
state.queue.length >= options.maxQueueDepth) {
|
|
239
|
-
throw claimedError(target, state.active, 'queue_too_deep');
|
|
240
|
-
}
|
|
241
|
-
await waitForNoClaims(target, {
|
|
242
|
-
timeout: options?.claimedTimeout,
|
|
243
|
-
pollInterval: options?.claimedPollInterval,
|
|
244
|
-
});
|
|
248
|
+
throw claimedError(target, state.active, 'model_claimed');
|
|
245
249
|
}
|
|
246
250
|
const commits = {
|
|
247
251
|
async create(commitOptions) {
|
|
@@ -382,7 +386,8 @@ export function createProtocolClient(options) {
|
|
|
382
386
|
body: JSON.stringify({
|
|
383
387
|
claimId,
|
|
384
388
|
target: claimOptions.target,
|
|
385
|
-
action
|
|
389
|
+
// Wire field stays `action`; public option is `reason`.
|
|
390
|
+
action: claimOptions.reason,
|
|
386
391
|
ttl: claimOptions.ttl,
|
|
387
392
|
queue: claimOptions.queue,
|
|
388
393
|
}),
|
|
@@ -408,7 +413,7 @@ export function createProtocolClient(options) {
|
|
|
408
413
|
return {
|
|
409
414
|
object: 'claim',
|
|
410
415
|
claimId: id,
|
|
411
|
-
|
|
416
|
+
reason: claimOptions.reason,
|
|
412
417
|
target: claimOptions.target,
|
|
413
418
|
release,
|
|
414
419
|
revoke: () => {
|
|
@@ -459,7 +464,7 @@ export function createProtocolClient(options) {
|
|
|
459
464
|
return {
|
|
460
465
|
data,
|
|
461
466
|
stamp: query.stamp ?? 0,
|
|
462
|
-
claims: query.claims ?? [],
|
|
467
|
+
claims: (query.claims ?? []).map(healClaimPhase),
|
|
463
468
|
};
|
|
464
469
|
}
|
|
465
470
|
/**
|
|
@@ -541,13 +546,14 @@ export function createProtocolClient(options) {
|
|
|
541
546
|
const body = await requestJson(claimPath(params.id), {
|
|
542
547
|
method: 'POST',
|
|
543
548
|
body: JSON.stringify({
|
|
544
|
-
|
|
549
|
+
// Wire field stays `action`; public option is `reason`.
|
|
550
|
+
action: params.reason ?? 'editing',
|
|
545
551
|
...(params.ttl !== undefined ? { ttl: params.ttl } : {}),
|
|
546
552
|
...(params.description !== undefined ? { description: params.description } : {}),
|
|
547
553
|
...(claimMeta(params) ? { meta: claimMeta(params) } : {}),
|
|
548
|
-
// `
|
|
554
|
+
// `queue` (default true) → queue behind the holder; false → fail-fast
|
|
549
555
|
// with AbloClaimedError (work-distribution dedup).
|
|
550
|
-
queue: params.
|
|
556
|
+
queue: params.queue ?? true,
|
|
551
557
|
}),
|
|
552
558
|
});
|
|
553
559
|
if (body.status === 'queued') {
|
|
@@ -573,7 +579,7 @@ export function createProtocolClient(options) {
|
|
|
573
579
|
...(params.range ? { range: params.range } : {}),
|
|
574
580
|
...(claimMeta(params) ? { meta: claimMeta(params) } : {}),
|
|
575
581
|
},
|
|
576
|
-
|
|
582
|
+
reason: params.reason ?? 'editing',
|
|
577
583
|
...(params.description ? { description: params.description } : {}),
|
|
578
584
|
data,
|
|
579
585
|
release,
|
|
@@ -588,11 +594,12 @@ export function createProtocolClient(options) {
|
|
|
588
594
|
release: releaseClaim,
|
|
589
595
|
state: async (params) => {
|
|
590
596
|
const res = await claimsForEntity(params);
|
|
591
|
-
|
|
597
|
+
const first = res.claims?.[0];
|
|
598
|
+
return first ? healClaimPhase(first) : null;
|
|
592
599
|
},
|
|
593
600
|
queue: async (params) => {
|
|
594
601
|
const res = await claimsForEntity(params);
|
|
595
|
-
return { object: 'list', data: res.queue ?? [] };
|
|
602
|
+
return { object: 'list', data: (res.queue ?? []).map(healClaimPhase) };
|
|
596
603
|
},
|
|
597
604
|
reorder: async (params) => {
|
|
598
605
|
await requestJson(`${claimPath(params.id)}/reorder`, {
|
|
@@ -656,6 +663,22 @@ export function createProtocolClient(options) {
|
|
|
656
663
|
claims,
|
|
657
664
|
commits,
|
|
658
665
|
model,
|
|
666
|
+
sessions: {
|
|
667
|
+
async create(params) {
|
|
668
|
+
// Stateless mint: the configured key IS the control-plane credential here
|
|
669
|
+
// (no startup `rk_` exchange runs on this client). Reuse the resolved base
|
|
670
|
+
// URL + fetch; the shared `mintSession` owns the two server doors.
|
|
671
|
+
const apiKey = await resolveApiKeyValue(configuredApiKey);
|
|
672
|
+
if (!apiKey) {
|
|
673
|
+
throw new AbloAuthenticationError('sessions.create requires a secret (sk_) API key — call it from your backend, not the browser.', { code: 'apikey_missing' });
|
|
674
|
+
}
|
|
675
|
+
return mintSession(params, {
|
|
676
|
+
apiKey,
|
|
677
|
+
baseUrl: apiBaseUrl,
|
|
678
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
679
|
+
});
|
|
680
|
+
},
|
|
681
|
+
},
|
|
659
682
|
async getAuthToken() {
|
|
660
683
|
// Mirror `authHeaders()`: a configured API key wins, else the
|
|
661
684
|
// construction-time auth token. Resolve the (possibly async) key setter.
|
package/dist/client/auth.d.ts
CHANGED
|
@@ -12,13 +12,20 @@
|
|
|
12
12
|
* explicit options so generated apps do not accrete hidden env knobs.
|
|
13
13
|
*/
|
|
14
14
|
/**
|
|
15
|
-
* Async callable that resolves
|
|
15
|
+
* Async callable that resolves the current credential. Mirrors the shape
|
|
16
16
|
* Anthropic / OpenAI / Stripe ship — used for credential rotation
|
|
17
|
-
* (e.g. AWS STS, GCP IAM, Vault)
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* (e.g. AWS STS, GCP IAM, Vault) AND the short-lived per-user browser
|
|
18
|
+
* path (mint a fresh `ek_`/`rk_` from the signed-in session). Re-exported
|
|
19
|
+
* from `./Ablo` so existing import paths work; defined here so this module
|
|
20
|
+
* has no circular dependency back to `Ablo.ts`.
|
|
21
|
+
*
|
|
22
|
+
* Contract: resolve a token; resolve `null` when the login itself is gone
|
|
23
|
+
* (terminal → the credential lifecycle treats this as `session_expired` and
|
|
24
|
+
* signs out); or THROW on a transient failure (→ back off and retry, never
|
|
25
|
+
* sign out). A long-lived static `apiKey` string needs none of this — it is
|
|
26
|
+
* used as-is. This is the single credential resolver the SDK supports.
|
|
20
27
|
*/
|
|
21
|
-
export type ApiKeySetter = () => Promise<string>;
|
|
28
|
+
export type ApiKeySetter = () => Promise<string | null>;
|
|
22
29
|
export interface AuthResolveInput {
|
|
23
30
|
/**
|
|
24
31
|
* The full options bag the caller passed to `Ablo()`. Resolvers
|
package/dist/client/auth.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* explicit options so generated apps do not accrete hidden env knobs.
|
|
13
13
|
*/
|
|
14
14
|
import { AbloAuthenticationError } from '../errors.js';
|
|
15
|
+
import { classifyCredentialKind } from '../auth/credentialPolicy.js';
|
|
15
16
|
/**
|
|
16
17
|
* Read `process.env` defensively. Works in browser (where `process`
|
|
17
18
|
* is undefined), Node, and edge runtimes that expose a partial
|
|
@@ -137,7 +138,7 @@ export function assertBrowserSafety(input) {
|
|
|
137
138
|
if (!input.dangerouslyAllowBrowser &&
|
|
138
139
|
inBrowser &&
|
|
139
140
|
typeof input.apiKey === 'string' &&
|
|
140
|
-
input.apiKey
|
|
141
|
+
classifyCredentialKind(input.apiKey) === 'secret') {
|
|
141
142
|
throw new AbloAuthenticationError("It looks like you're running in a browser-like environment.\n\n" +
|
|
142
143
|
'This is disabled by default — your secret API key would be ' +
|
|
143
144
|
"exposed to every visitor's network tab. If you understand the risks " +
|