@abloatai/ablo 0.17.0 → 0.19.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 +70 -0
- package/dist/BaseSyncedStore.d.ts +5 -4
- package/dist/BaseSyncedStore.js +38 -14
- package/dist/NetworkMonitor.js +3 -1
- package/dist/ObjectPool.d.ts +14 -1
- package/dist/ObjectPool.js +8 -1
- package/dist/SyncClient.js +7 -1
- package/dist/SyncEngineContext.js +2 -0
- package/dist/ai-sdk/coordination-context.d.ts +2 -1
- package/dist/ai-sdk/coordination-context.js +3 -3
- package/dist/ai-sdk/index.d.ts +3 -4
- package/dist/ai-sdk/index.js +2 -3
- package/dist/ai-sdk/wrap.d.ts +13 -18
- package/dist/ai-sdk/wrap.js +2 -6
- package/dist/cli.cjs +459 -205
- package/dist/client/Ablo.d.ts +116 -23
- package/dist/client/Ablo.js +128 -38
- package/dist/client/ApiClient.d.ts +2 -2
- package/dist/client/ApiClient.js +27 -32
- package/dist/client/auth.d.ts +26 -0
- package/dist/client/auth.js +209 -1
- package/dist/client/createModelProxy.d.ts +16 -11
- package/dist/client/createModelProxy.js +15 -15
- package/dist/client/sessionMint.d.ts +10 -0
- package/dist/client/sessionMint.js +8 -1
- package/dist/coordination/schema.d.ts +10 -10
- package/dist/coordination/schema.js +7 -8
- package/dist/coordination/trace.d.ts +91 -0
- package/dist/coordination/trace.js +147 -0
- package/dist/core/StoreManager.js +3 -1
- package/dist/errorCodes.d.ts +2 -0
- package/dist/errorCodes.js +2 -0
- package/dist/errors.d.ts +10 -2
- package/dist/errors.js +20 -9
- package/dist/index.d.ts +7 -1
- package/dist/index.js +12 -0
- package/dist/interfaces/index.d.ts +45 -2
- package/dist/policy/types.d.ts +33 -9
- package/dist/policy/types.js +42 -11
- package/dist/react/AbloProvider.d.ts +2 -2
- package/dist/schema/ddl.d.ts +36 -0
- package/dist/schema/ddl.js +66 -0
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +2 -0
- package/dist/sync/HydrationCoordinator.js +7 -3
- package/dist/sync/SyncWebSocket.d.ts +11 -2
- package/dist/sync/SyncWebSocket.js +68 -4
- package/dist/sync/awaitClaimGrant.js +9 -2
- package/dist/sync/createClaimStream.d.ts +9 -2
- package/dist/sync/createClaimStream.js +41 -7
- package/dist/sync/participants.d.ts +12 -11
- package/dist/sync/participants.js +5 -5
- package/dist/transactions/TransactionQueue.d.ts +1 -0
- package/dist/transactions/TransactionQueue.js +40 -3
- package/dist/types/streams.d.ts +57 -135
- package/dist/wire/errorEnvelope.d.ts +13 -0
- package/docs/coordination.md +16 -0
- package/docs/debugging.md +194 -0
- package/docs/index.md +1 -0
- package/package.json +1 -1
- package/dist/ai-sdk/claim-broadcast.d.ts +0 -83
- package/dist/ai-sdk/claim-broadcast.js +0 -79
package/dist/client/Ablo.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ import type { SyncWebSocket } from '../sync/SyncWebSocket.js';
|
|
|
29
29
|
import type { SyncGroupInput } from '../schema/roles.js';
|
|
30
30
|
import { type SyncStatus } from '../BaseSyncedStore.js';
|
|
31
31
|
import type { ClaimStream, ClaimWaitOptions, PresenceStream, Snapshot } from '../types/streams.js';
|
|
32
|
-
import type {
|
|
32
|
+
import type { Claim, Duration } from '../types/streams.js';
|
|
33
33
|
import { type AbloApi, type AbloApiClientOptions, type AbloApiClaims } from './ApiClient.js';
|
|
34
34
|
import { type AbloHttpClient, type AbloHttpClientOptions } from './httpClient.js';
|
|
35
35
|
/**
|
|
@@ -96,18 +96,19 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
96
96
|
*/
|
|
97
97
|
apiKey?: string | ApiKeySetter | null | undefined;
|
|
98
98
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
99
|
+
* @deprecated The direct connector lets Ablo dial INTO your Postgres and write to
|
|
100
|
+
* it — the operate-their-database posture we are moving off. Ablo is Stripe-shaped:
|
|
101
|
+
* it hosts only the transaction log (the ordered sync_deltas) + coordination, never
|
|
102
|
+
* your data; your rows always live in your own database. Use the signed Data Source
|
|
103
|
+
* endpoint instead — keep `DATABASE_URL` in your app, expose `dataSource(...)`, and
|
|
104
|
+
* let your server own the write while Ablo coordinates the sync stream. To keep the
|
|
105
|
+
* log in your infra too, self-host the engine. See
|
|
106
|
+
* docs/plans/stripe-shaped-storage-posture.md.
|
|
101
107
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* SERVER-ONLY: this carries credentials, so it is never sent from the browser
|
|
107
|
-
* — constructing a client with `databaseUrl` and `dangerouslyAllowBrowser`
|
|
108
|
-
* throws. If you opt into this connector, provide a NON-superuser,
|
|
109
|
-
* non-`BYPASSRLS` role; the direct connector rejects privileged roles that
|
|
110
|
-
* cannot enforce RLS.
|
|
108
|
+
* Still honored at runtime for back-compat. SERVER-ONLY: it carries credentials, so
|
|
109
|
+
* it is never sent from the browser — constructing a client with `databaseUrl` and
|
|
110
|
+
* `dangerouslyAllowBrowser` throws. If you use it, provide a NON-superuser,
|
|
111
|
+
* non-`BYPASSRLS` role; the connector rejects privileged roles that cannot enforce RLS.
|
|
111
112
|
*/
|
|
112
113
|
databaseUrl?: string | null | undefined;
|
|
113
114
|
/**
|
|
@@ -133,6 +134,22 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
133
134
|
* @default 'websocket'
|
|
134
135
|
*/
|
|
135
136
|
transport?: 'websocket' | 'http' | undefined;
|
|
137
|
+
/**
|
|
138
|
+
* Turn Ablo's diagnostic logging on/off. `true` surfaces the `[Ablo]`
|
|
139
|
+
* coordination trace — claims requested / queued / granted / released, agent
|
|
140
|
+
* handovers, connection state — so you can SEE the human+agent coordination
|
|
141
|
+
* you built while debugging. Omitted/`false` keeps the quiet default (only
|
|
142
|
+
* warnings + errors). For a middle ground use {@link logLevel}. Env override:
|
|
143
|
+
* `ABLO_LOG_LEVEL`. Ignored if a custom logger is supplied.
|
|
144
|
+
*/
|
|
145
|
+
debug?: boolean | undefined;
|
|
146
|
+
/**
|
|
147
|
+
* Log threshold for the default `[Ablo]` logger (takes precedence over
|
|
148
|
+
* {@link debug}). `'info'` = coordination + connection events without the
|
|
149
|
+
* per-model registration firehose; `'debug'` = everything; `'warn'` (default)
|
|
150
|
+
* = warnings + errors only; `'silent'` = nothing. Env override: `ABLO_LOG_LEVEL`.
|
|
151
|
+
*/
|
|
152
|
+
logLevel?: 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined;
|
|
136
153
|
/**
|
|
137
154
|
* Bearer auth token. Hosted-cloud consumers pass `apiKey`; self-hosted
|
|
138
155
|
* deployments may pass a bearer token minted by their own auth layer.
|
|
@@ -242,8 +259,24 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
242
259
|
* `apiKey` only; the server handles capability issuance.
|
|
243
260
|
*/
|
|
244
261
|
capabilityToken?: string;
|
|
245
|
-
/** Custom logger (default: console) */
|
|
262
|
+
/** Custom logger (default: console). Supplying one bypasses {@link debug}/{@link logLevel}. */
|
|
246
263
|
logger?: SyncLogger;
|
|
264
|
+
/**
|
|
265
|
+
* Turn Ablo's diagnostic logging on/off. `true` surfaces the `[Ablo]`
|
|
266
|
+
* coordination trace — claims acquired / queued / granted / released, agent
|
|
267
|
+
* handovers, connection state — plus internal lifecycle, so you can SEE the
|
|
268
|
+
* human+agent coordination you built. Omitted/`false` keeps the quiet default
|
|
269
|
+
* (only warnings + errors). For a middle ground use {@link logLevel}.
|
|
270
|
+
* Env override: `ABLO_LOG_LEVEL`. Ignored if a custom {@link logger} is passed.
|
|
271
|
+
*/
|
|
272
|
+
debug?: boolean;
|
|
273
|
+
/**
|
|
274
|
+
* Log threshold for the default `[Ablo]` logger (takes precedence over
|
|
275
|
+
* {@link debug}). `'info'` = coordination + connection events without the
|
|
276
|
+
* per-model registration firehose; `'debug'` = everything; `'warn'` (default)
|
|
277
|
+
* = warnings + errors only; `'silent'` = nothing. Env override: `ABLO_LOG_LEVEL`.
|
|
278
|
+
*/
|
|
279
|
+
logLevel?: 'debug' | 'info' | 'warn' | 'error' | 'silent';
|
|
247
280
|
/** ObjectPool size limit (default: 10000) */
|
|
248
281
|
maxPoolSize?: number;
|
|
249
282
|
/**
|
|
@@ -371,7 +404,7 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
371
404
|
* `create({ data })` / `update({ id, data })` / `delete({ id })` — writes
|
|
372
405
|
* `claim({ id })` — durable claim handle for coordinated writes
|
|
373
406
|
*/
|
|
374
|
-
export type { LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams,
|
|
407
|
+
export type { LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, Claim, ModelOperations, } from './createModelProxy.js';
|
|
375
408
|
import type { ModelOperations, ClaimOptions, ClaimParams, ClaimReadApi, AwaitedClaimMethod, ServerReadOptions } from './createModelProxy.js';
|
|
376
409
|
export type ModelOperationAction = 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
|
|
377
410
|
export type CommitWait = 'queued' | 'confirmed';
|
|
@@ -396,7 +429,7 @@ export interface ModelReadOptions extends ClaimedOptions {
|
|
|
396
429
|
export interface ClaimCreateOptions {
|
|
397
430
|
readonly target: ModelTarget;
|
|
398
431
|
/** Human-readable phase shown to peers — `'editing'`, `'writing'`. The same
|
|
399
|
-
* word on every claim surface
|
|
432
|
+
* word on every claim surface. */
|
|
400
433
|
readonly reason: string;
|
|
401
434
|
readonly ttl?: Duration;
|
|
402
435
|
/**
|
|
@@ -442,7 +475,7 @@ export interface CommitCreateOptions {
|
|
|
442
475
|
* against concurrent edits without re-stating the watermark by hand.
|
|
443
476
|
* Explicit `readAt`/`onStale` on the options win.
|
|
444
477
|
*/
|
|
445
|
-
readonly claim?:
|
|
478
|
+
readonly claim?: Claim<Record<string, unknown>> | null;
|
|
446
479
|
readonly operation?: CommitOperationInput;
|
|
447
480
|
readonly operations?: readonly CommitOperationInput[];
|
|
448
481
|
readonly wait?: CommitWait;
|
|
@@ -481,7 +514,7 @@ export interface CommitResource {
|
|
|
481
514
|
create(options: CommitCreateOptions): Promise<CommitReceipt>;
|
|
482
515
|
}
|
|
483
516
|
export interface ClaimResource extends ClaimStream {
|
|
484
|
-
create(options: ClaimCreateOptions): Promise<
|
|
517
|
+
create(options: ClaimCreateOptions): Promise<Claim>;
|
|
485
518
|
list(target?: Partial<ModelTarget>): readonly ModelClaim[];
|
|
486
519
|
waitFor(target: Partial<ModelTarget>, options?: ClaimWaitOptions): Promise<void>;
|
|
487
520
|
}
|
|
@@ -493,7 +526,7 @@ export interface ModelMutationOptions extends ClaimedOptions {
|
|
|
493
526
|
readonly readAt?: number | null;
|
|
494
527
|
readonly onStale?: 'reject' | 'overwrite' | 'notify' | null;
|
|
495
528
|
readonly wait?: CommitWait;
|
|
496
|
-
readonly claim?:
|
|
529
|
+
readonly claim?: Claim | ClaimOptions | null;
|
|
497
530
|
}
|
|
498
531
|
/**
|
|
499
532
|
* The HTTP/stateless claim surface. Normal tools usually put `claim` directly
|
|
@@ -511,7 +544,7 @@ export interface ModelMutationOptions extends ClaimedOptions {
|
|
|
511
544
|
* wrapper that statelessness forces. `claim({ id })` is identical (already async
|
|
512
545
|
* on both); `state`/`queue`/`reorder`/`release` are the awaited form.
|
|
513
546
|
*/
|
|
514
|
-
export type HttpClaimApi<T = Record<string, unknown>> = ((params: ClaimParams<T>) => Promise<
|
|
547
|
+
export type HttpClaimApi<T = Record<string, unknown>> = ((params: ClaimParams<T>) => Promise<Claim<T>>) & {
|
|
515
548
|
[K in keyof ClaimReadApi<T>]: AwaitedClaimMethod<ClaimReadApi<T>[K]>;
|
|
516
549
|
};
|
|
517
550
|
export interface ModelClient<T = Record<string, unknown>> {
|
|
@@ -614,6 +647,38 @@ export interface CreateAgentSessionParams<S extends SchemaRecord> {
|
|
|
614
647
|
* `{ user }` for a full-authority end-user session (`ek_`) or `{ agent, can }`
|
|
615
648
|
* for a scoped agent session (`rk_`). */
|
|
616
649
|
export type CreateSessionParams<S extends SchemaRecord> = CreateUserSessionParams | CreateAgentSessionParams<S>;
|
|
650
|
+
/** Params for {@link Ablo.agents}.create — a flattened agent descriptor (no
|
|
651
|
+
* `{ agent }` discriminator: `agents.create` only ever mints an agent). Unlike
|
|
652
|
+
* {@link CreateSessionParams} it resolves to a connected, scoped {@link Ablo}
|
|
653
|
+
* client rather than a raw token. */
|
|
654
|
+
export interface CreateAgentClientParams<S extends SchemaRecord> {
|
|
655
|
+
/** Wire participant identity (`agent:<id>`) — what claim exclusion and the
|
|
656
|
+
* FIFO queue gate on. OMIT to get a fresh `crypto.randomUUID()`: a distinct,
|
|
657
|
+
* independent participant (the default, and what you want for concurrent
|
|
658
|
+
* agents). Pass a STABLE string only when one logical agent must re-attach
|
|
659
|
+
* to its own held claims across reconnects/restarts. */
|
|
660
|
+
id?: string;
|
|
661
|
+
/** Human-readable label for logs / attribution (carried in `userMeta.name`).
|
|
662
|
+
* INDEPENDENT of `id`: two agents that share a `name` still receive distinct
|
|
663
|
+
* ids and coordinate as SEPARATE participants — `name` never derives or
|
|
664
|
+
* collapses identity. */
|
|
665
|
+
name?: string;
|
|
666
|
+
/** Per-model operation allowlist, typed against the schema's model names. */
|
|
667
|
+
can: {
|
|
668
|
+
[M in keyof S & string]?: readonly SessionOperation[];
|
|
669
|
+
};
|
|
670
|
+
/** Sync groups this agent may subscribe to — typed (`'default'` or
|
|
671
|
+
* `<namespace>:<id>`). Omit for the server default (org anchor + the
|
|
672
|
+
* agent's own anchor). */
|
|
673
|
+
syncGroups?: readonly SyncGroupInput[];
|
|
674
|
+
/** Token lifetime in seconds. Defaults to 900 (15m); the returned client
|
|
675
|
+
* auto-re-mints before expiry, so a long-running agent never handles
|
|
676
|
+
* rotation itself. */
|
|
677
|
+
ttlSeconds?: number;
|
|
678
|
+
/** Extra opaque identity blob echoed on the session scope. Merged with
|
|
679
|
+
* `name` (the `name` param wins if you also set `userMeta.name`). */
|
|
680
|
+
userMeta?: Record<string, unknown>;
|
|
681
|
+
}
|
|
617
682
|
/** A minted session token — the Stripe ephemeral-key / Supabase session
|
|
618
683
|
* resource. `token` is the secret the holder presents as its bearer. */
|
|
619
684
|
export interface AbloSession {
|
|
@@ -734,6 +799,35 @@ export type Ablo<S extends SchemaRecord> = {
|
|
|
734
799
|
sessions: {
|
|
735
800
|
create(params: CreateSessionParams<S>): Promise<AbloSession>;
|
|
736
801
|
};
|
|
802
|
+
/**
|
|
803
|
+
* Mint a scoped **agent identity** and return a ready-to-use client bound to
|
|
804
|
+
* it — the `ablo.<resource>.<verb>` shape for the agent use case. One call
|
|
805
|
+
* replaces `sessions.create({ agent })` + constructing a second
|
|
806
|
+
* `Ablo({ apiKey: token })`:
|
|
807
|
+
*
|
|
808
|
+
* ```ts
|
|
809
|
+
* const agent = await ablo.agents.create({
|
|
810
|
+
* name: 'researcher', // readable label (optional)
|
|
811
|
+
* can: { documents: ['read', 'update'] },
|
|
812
|
+
* // id omitted → a fresh uuid: a distinct, independent participant
|
|
813
|
+
* });
|
|
814
|
+
* await agent.documents.update({ id, data, claim });
|
|
815
|
+
* await agent.dispose(); // when the agent is done
|
|
816
|
+
* ```
|
|
817
|
+
*
|
|
818
|
+
* Server-side only (requires the `sk_` secret key, like `sessions.create`);
|
|
819
|
+
* throws `AbloAuthenticationError` in the browser. The returned client holds
|
|
820
|
+
* its own auto-refreshing `rk_`, so a long run never hits token expiry, and
|
|
821
|
+
* the `sk_` never leaves this process. Each call is a DISTINCT participant by
|
|
822
|
+
* default (omit `id` → fresh uuid), so even two agents sharing a `name` queue
|
|
823
|
+
* behind one another on a contended row — `name` is display only and never
|
|
824
|
+
* collapses identity. Humans don't get a server-built client — ship them a
|
|
825
|
+
* token via `sessions.create({ user })`. Need the raw token for revocation,
|
|
826
|
+
* or a stable re-attachable id? Use `sessions.create({ agent })` / pass `id`.
|
|
827
|
+
*/
|
|
828
|
+
agents: {
|
|
829
|
+
create(params: CreateAgentClientParams<S>): Promise<Ablo<S>>;
|
|
830
|
+
};
|
|
737
831
|
/**
|
|
738
832
|
* The organization this client resolved to — `null` until `ready()`
|
|
739
833
|
* completes. Use it instead of scraping CLI output or hardcoding env vars:
|
|
@@ -975,7 +1069,7 @@ export declare namespace Ablo {
|
|
|
975
1069
|
type RotatedCapability = import('./ApiClient.js').RotatedCapability;
|
|
976
1070
|
type IfClaimedPolicy = import('./Ablo.js').IfClaimedPolicy;
|
|
977
1071
|
type ClaimedOptions = import('./Ablo.js').ClaimedOptions;
|
|
978
|
-
type
|
|
1072
|
+
type ClaimTarget = _Streams.ClaimTarget;
|
|
979
1073
|
type PresenceTarget = _Streams.PresenceTarget;
|
|
980
1074
|
type TargetRange = _Streams.TargetRange;
|
|
981
1075
|
type Duration = _Streams.Duration;
|
|
@@ -983,8 +1077,7 @@ export declare namespace Ablo {
|
|
|
983
1077
|
type ClaimStream = _Streams.ClaimStream;
|
|
984
1078
|
type Peer = _Streams.Peer;
|
|
985
1079
|
type Activity = _Streams.Activity;
|
|
986
|
-
type
|
|
987
|
-
type ClaimHandle = _Streams.ClaimHandle;
|
|
1080
|
+
type Claim = _Streams.Claim;
|
|
988
1081
|
type ClaimRejection = _Streams.ClaimRejection;
|
|
989
1082
|
type ClaimLost = _Streams.ClaimLost;
|
|
990
1083
|
type Snapshot<TSchema extends _SchemaTypes.Schema = _SchemaTypes.Schema, K extends keyof TSchema['models'] = keyof TSchema['models']> = _Streams.Snapshot<TSchema, K>;
|
|
@@ -1021,7 +1114,7 @@ export declare namespace Ablo {
|
|
|
1021
1114
|
type Client = import('./Ablo.js').CommitResource;
|
|
1022
1115
|
}
|
|
1023
1116
|
namespace Claim {
|
|
1024
|
-
type Handle = import('./Ablo.js').
|
|
1117
|
+
type Handle = import('./Ablo.js').Claim;
|
|
1025
1118
|
type CreateOptions = import('./Ablo.js').ClaimCreateOptions;
|
|
1026
1119
|
type WaitOptions = import('./Ablo.js').ClaimWaitOptions;
|
|
1027
1120
|
type Client = import('./Ablo.js').ClaimResource;
|
package/dist/client/Ablo.js
CHANGED
|
@@ -42,7 +42,7 @@ import { createProtocolClient, } from './ApiClient.js';
|
|
|
42
42
|
// Value import is cycle-safe: httpClient.js only value-imports ApiClient.js,
|
|
43
43
|
// which imports this module type-only.
|
|
44
44
|
import { createAbloHttpClient, } from './httpClient.js';
|
|
45
|
-
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfDatabaseUrlEnvIgnored, } from './auth.js';
|
|
45
|
+
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfCliKeyMismatch, warnIfDatabaseUrlEnvIgnored, warnIfDatabaseUrlDeprecated, } from './auth.js';
|
|
46
46
|
import { registerDataSource } from './registerDataSource.js';
|
|
47
47
|
import { shouldUseInMemoryPersistence, } from './persistence.js';
|
|
48
48
|
import { createModelProxy } from './createModelProxy.js';
|
|
@@ -519,7 +519,17 @@ function createDynamicModelClass(modelName, jsonSubFields, fieldNames, computed,
|
|
|
519
519
|
return ModelClass;
|
|
520
520
|
}
|
|
521
521
|
const LOG_LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40, silent: 99 };
|
|
522
|
-
|
|
522
|
+
/**
|
|
523
|
+
* Resolve the effective level. Precedence: explicit `logLevel` option →
|
|
524
|
+
* `debug: true` (⇒ debug) → `ABLO_LOG_LEVEL` env → default `warn`. `debug: false`
|
|
525
|
+
* / omitted just means "don't raise the level" — it falls through to env/default
|
|
526
|
+
* rather than force-silencing an ops-set env override.
|
|
527
|
+
*/
|
|
528
|
+
function resolveLogLevel(opts) {
|
|
529
|
+
if (opts?.logLevel && opts.logLevel in LOG_LEVEL_RANK)
|
|
530
|
+
return opts.logLevel;
|
|
531
|
+
if (opts?.debug === true)
|
|
532
|
+
return 'debug';
|
|
523
533
|
// `globalThis.process` guard keeps this safe in browser/edge runtimes that
|
|
524
534
|
// have no `process` binding — there we fall through to the default.
|
|
525
535
|
const raw = globalThis.process?.env?.ABLO_LOG_LEVEL;
|
|
@@ -528,12 +538,16 @@ function resolveLogLevel() {
|
|
|
528
538
|
return normalized;
|
|
529
539
|
return 'warn';
|
|
530
540
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
541
|
+
/**
|
|
542
|
+
* Build the default logger, gated at `level` and prefixed `[Ablo]` so a creator
|
|
543
|
+
* with a console full of other tools' logs can see at a glance what's ours.
|
|
544
|
+
*/
|
|
545
|
+
function createConsoleLogger(level) {
|
|
546
|
+
const threshold = LOG_LEVEL_RANK[level];
|
|
547
|
+
const emit = (lvl, fn, args) => {
|
|
548
|
+
if (typeof console === 'undefined' || LOG_LEVEL_RANK[lvl] < threshold)
|
|
535
549
|
return;
|
|
536
|
-
fn('[
|
|
550
|
+
fn('[Ablo]', ...args);
|
|
537
551
|
};
|
|
538
552
|
return {
|
|
539
553
|
debug: (...args) => emit('debug', console.debug, args),
|
|
@@ -541,7 +555,7 @@ const consoleLogger = (() => {
|
|
|
541
555
|
warn: (...args) => emit('warn', console.warn, args),
|
|
542
556
|
error: (...args) => emit('error', console.error, args),
|
|
543
557
|
};
|
|
544
|
-
}
|
|
558
|
+
}
|
|
545
559
|
// `readProcessEnv` lives in `./auth` alongside the other resolvers
|
|
546
560
|
// that read it. Re-exported there for use elsewhere in the file.
|
|
547
561
|
// ── Default mutation executor (wire: `commit` frame over WebSocket) ──────
|
|
@@ -725,10 +739,15 @@ export function Ablo(options) {
|
|
|
725
739
|
databaseUrl: configuredDatabaseUrl,
|
|
726
740
|
dangerouslyAllowBrowser: options.dangerouslyAllowBrowser,
|
|
727
741
|
});
|
|
728
|
-
|
|
742
|
+
// Custom logger wins; otherwise build the default `[Ablo]` logger at the level
|
|
743
|
+
// resolved from `debug`/`logLevel`/`ABLO_LOG_LEVEL` (default `warn`).
|
|
744
|
+
const logger = internalOptions.logger ??
|
|
745
|
+
createConsoleLogger(resolveLogLevel({ debug: options.debug, logLevel: options.logLevel }));
|
|
729
746
|
// Nudge (once) if a stray DATABASE_URL is in the env but `databaseUrl` wasn't
|
|
730
747
|
// passed — the env value is no longer auto-adopted (see resolveDatabaseUrl).
|
|
731
748
|
warnIfDatabaseUrlEnvIgnored(authInput, (m) => logger.warn(m));
|
|
749
|
+
warnIfDatabaseUrlDeprecated(authInput, (m) => logger.warn(m));
|
|
750
|
+
void warnIfCliKeyMismatch(authInput, (m) => logger.warn(m));
|
|
732
751
|
const schema = options.schema;
|
|
733
752
|
const url = resolveBaseURL(authInput);
|
|
734
753
|
// 1. Derive config from schema
|
|
@@ -877,6 +896,19 @@ export function Ablo(options) {
|
|
|
877
896
|
store.syncStatus.state = 'error';
|
|
878
897
|
store.syncStatus.error = _validationError;
|
|
879
898
|
}
|
|
899
|
+
// Deprecated identity overrides are a silent no-op under hosted cloud: when an
|
|
900
|
+
// `apiKey` is configured the SERVER derives participant kind + id from the
|
|
901
|
+
// key's scope, so `kind` / `agentId` passed here are ignored. Setting them and
|
|
902
|
+
// trusting them is the trap (you think you're an agent; the key says user).
|
|
903
|
+
// Warn loudly rather than removing the fields — `agentId` is still load-bearing
|
|
904
|
+
// on the self-hosted path (no apiKey; paired with `capabilityToken`).
|
|
905
|
+
if (configuredApiKey && (internalOptions.kind || internalOptions.agentId)) {
|
|
906
|
+
logger.warn('Ablo: `kind` / `agentId` are ignored when an `apiKey` is configured — ' +
|
|
907
|
+
'the server derives participant identity from the key’s scope. Remove ' +
|
|
908
|
+
'them (or mint a scoped session via `ablo.sessions.create({ agent })` ' +
|
|
909
|
+
'for a distinct agent identity). They apply only to the self-hosted ' +
|
|
910
|
+
'`capabilityToken` path.');
|
|
911
|
+
}
|
|
880
912
|
// 7. The ready() promise drives the BaseSyncedStore.initialize() generator
|
|
881
913
|
// to completion. First call kicks off the initialization; subsequent
|
|
882
914
|
// calls return the same promise (idempotent).
|
|
@@ -1100,7 +1132,7 @@ export function Ablo(options) {
|
|
|
1100
1132
|
return (typeof value === 'object' &&
|
|
1101
1133
|
value !== null &&
|
|
1102
1134
|
value.object === 'claim' &&
|
|
1103
|
-
typeof value.
|
|
1135
|
+
typeof value.id === 'string');
|
|
1104
1136
|
}
|
|
1105
1137
|
function normalizeCommitOperation(op, defaults) {
|
|
1106
1138
|
const model = op.model ?? op.target?.model;
|
|
@@ -1135,13 +1167,13 @@ export function Ablo(options) {
|
|
|
1135
1167
|
const description = descriptionFromMeta(claim.target.meta);
|
|
1136
1168
|
return {
|
|
1137
1169
|
id: claim.id,
|
|
1138
|
-
actor: claim.heldBy,
|
|
1139
|
-
participantKind: claim.participantKind,
|
|
1170
|
+
actor: claim.heldBy ?? "",
|
|
1171
|
+
participantKind: claim.participantKind ?? "user",
|
|
1140
1172
|
reason: claim.reason,
|
|
1141
1173
|
...(description ? { description } : {}),
|
|
1142
1174
|
field: claim.target.field,
|
|
1143
1175
|
status: 'active',
|
|
1144
|
-
expiresAt: claim.expiresAt,
|
|
1176
|
+
expiresAt: claim.expiresAt ?? 0,
|
|
1145
1177
|
target: {
|
|
1146
1178
|
model: claim.target.type,
|
|
1147
1179
|
id: claim.target.id,
|
|
@@ -1155,14 +1187,14 @@ export function Ablo(options) {
|
|
|
1155
1187
|
function modelClaimFromQueued(claim) {
|
|
1156
1188
|
return {
|
|
1157
1189
|
id: claim.id,
|
|
1158
|
-
actor: claim.heldBy,
|
|
1159
|
-
participantKind: claim.participantKind,
|
|
1190
|
+
actor: claim.heldBy ?? "",
|
|
1191
|
+
participantKind: claim.participantKind ?? "user",
|
|
1160
1192
|
reason: claim.reason,
|
|
1161
1193
|
...(claim.description ? { description: claim.description } : {}),
|
|
1162
1194
|
field: claim.target.field,
|
|
1163
1195
|
status: 'queued',
|
|
1164
1196
|
position: claim.position,
|
|
1165
|
-
expiresAt: claim.expiresAt,
|
|
1197
|
+
expiresAt: claim.expiresAt ?? 0,
|
|
1166
1198
|
target: {
|
|
1167
1199
|
model: claim.target.type,
|
|
1168
1200
|
id: claim.target.id,
|
|
@@ -1254,11 +1286,11 @@ export function Ablo(options) {
|
|
|
1254
1286
|
}
|
|
1255
1287
|
function wrapClaimHandle(claim, waited = false) {
|
|
1256
1288
|
const release = async () => {
|
|
1257
|
-
claim.revoke();
|
|
1289
|
+
claim.revoke?.();
|
|
1258
1290
|
};
|
|
1259
1291
|
return {
|
|
1260
1292
|
object: 'claim',
|
|
1261
|
-
|
|
1293
|
+
id: claim.id,
|
|
1262
1294
|
reason: claim.reason,
|
|
1263
1295
|
target: claim.target,
|
|
1264
1296
|
waited,
|
|
@@ -1292,7 +1324,7 @@ export function Ablo(options) {
|
|
|
1292
1324
|
const ws = store.getSyncWebSocket();
|
|
1293
1325
|
if (ws) {
|
|
1294
1326
|
try {
|
|
1295
|
-
({ waited } = await awaitClaimGrant(ws, claim.
|
|
1327
|
+
({ waited } = await awaitClaimGrant(ws, claim.id, {
|
|
1296
1328
|
timeoutMs: claimOptions.waitTimeoutMs,
|
|
1297
1329
|
maxQueueDepth: claimOptions.maxQueueDepth,
|
|
1298
1330
|
}));
|
|
@@ -1301,7 +1333,7 @@ export function Ablo(options) {
|
|
|
1301
1333
|
// Gave up waiting (queue too deep, timed out, or lost) — abandon
|
|
1302
1334
|
// the queued claim so we don't leave a phantom entry in the
|
|
1303
1335
|
// line that would block or mislead other claimers.
|
|
1304
|
-
claim.revoke();
|
|
1336
|
+
claim.revoke?.();
|
|
1305
1337
|
throw err;
|
|
1306
1338
|
}
|
|
1307
1339
|
}
|
|
@@ -1413,7 +1445,7 @@ export function Ablo(options) {
|
|
|
1413
1445
|
onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
|
|
1414
1446
|
});
|
|
1415
1447
|
const wait = commitOptions.wait ?? 'confirmed';
|
|
1416
|
-
const claimId = normalizeClaimId(commitOptions.claimRef) ?? claim?.
|
|
1448
|
+
const claimId = normalizeClaimId(commitOptions.claimRef) ?? claim?.id;
|
|
1417
1449
|
void claimId; // The current wire clears claims by entity after commit.
|
|
1418
1450
|
// Route through the TransactionQueue's commit lane so the call
|
|
1419
1451
|
// tolerates WS disconnects: the envelope stays in memory until
|
|
@@ -1562,6 +1594,34 @@ export function Ablo(options) {
|
|
|
1562
1594
|
async function controlPlaneApiKey() {
|
|
1563
1595
|
return resolveApiKeyValue(configuredApiKey);
|
|
1564
1596
|
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Resolve the control-plane context a session/agent mint needs (sk_ +
|
|
1599
|
+
* bootstrap base URL + the schema-key→typename map the Hub gates on).
|
|
1600
|
+
* Shared by `sessions.create` and `agents.create` so the two mint doors
|
|
1601
|
+
* can never drift on how a token is minted. Throws if no `sk_` is present —
|
|
1602
|
+
* minting is a backend-only operation.
|
|
1603
|
+
*/
|
|
1604
|
+
async function buildMintContext(resource) {
|
|
1605
|
+
const apiKey = await controlPlaneApiKey();
|
|
1606
|
+
if (!apiKey) {
|
|
1607
|
+
throw new AbloAuthenticationError(`${resource} requires a secret (sk_) API key — call it from your backend, not the browser.`, { code: 'apikey_missing' });
|
|
1608
|
+
}
|
|
1609
|
+
return {
|
|
1610
|
+
apiKey,
|
|
1611
|
+
baseUrl: resolveBootstrapBaseUrl({
|
|
1612
|
+
url,
|
|
1613
|
+
bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
|
|
1614
|
+
}),
|
|
1615
|
+
...(internalOptions.fetch ? { fetch: internalOptions.fetch } : {}),
|
|
1616
|
+
// Map every `can` schema-key to the wire typename the Hub gates on, so a
|
|
1617
|
+
// typename override (`documents` → `Document`) doesn't mint a capability
|
|
1618
|
+
// the server then denies. See `MintSessionContext`.
|
|
1619
|
+
modelTypenames: Object.fromEntries(Object.entries(schema.models).map(([key, def]) => [
|
|
1620
|
+
key,
|
|
1621
|
+
def.typename ?? key,
|
|
1622
|
+
])),
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1565
1625
|
const engine = {
|
|
1566
1626
|
...modelProxies,
|
|
1567
1627
|
ready,
|
|
@@ -1614,23 +1674,53 @@ export function Ablo(options) {
|
|
|
1614
1674
|
// agent credential silently replacing the secret key on control-plane
|
|
1615
1675
|
// calls is how humans get minted as agents — attribution is the product.
|
|
1616
1676
|
async create(params) {
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1677
|
+
// Both mint doors (`{ user }` → /auth/ephemeral-keys → `ek_`,
|
|
1678
|
+
// `{ agent, can }` → /auth/capability → scoped `rk_`) resolve their
|
|
1679
|
+
// control-plane context through the shared `buildMintContext`, so this
|
|
1680
|
+
// client, `agents.create`, and the stateless HTTP client can never drift
|
|
1681
|
+
// on how a token is minted.
|
|
1682
|
+
return mintSession(params, await buildMintContext('sessions.create'));
|
|
1683
|
+
},
|
|
1684
|
+
},
|
|
1685
|
+
// Mint a scoped agent IDENTITY and hand back a connected client bound to it
|
|
1686
|
+
// — `sessions.create({ agent })` + `Ablo({ apiKey })` fused into one call,
|
|
1687
|
+
// for agents that run in THIS (sk_-holding) process. Omitting `id` yields a
|
|
1688
|
+
// fresh uuid per call, so concurrent agents are distinct participants that
|
|
1689
|
+
// queue behind each other (even when they share a `name`). Humans don't get
|
|
1690
|
+
// a server-built client — ship them a token via `sessions.create({ user })`.
|
|
1691
|
+
agents: {
|
|
1692
|
+
async create(params) {
|
|
1693
|
+
// Distinct participant by default: omit `id` → a fresh uuid, so even two
|
|
1694
|
+
// agents that share a `name` are INDEPENDENT participants and queue
|
|
1695
|
+
// behind one another. `name` is display only (→ userMeta.name); it never
|
|
1696
|
+
// derives the id. Pass an explicit `id` only to re-attach an agent to
|
|
1697
|
+
// its own held claims.
|
|
1698
|
+
const id = params.id ?? globalThis.crypto.randomUUID();
|
|
1699
|
+
const userMeta = params.name !== undefined ? { ...params.userMeta, name: params.name } : params.userMeta;
|
|
1700
|
+
const sessionParams = {
|
|
1701
|
+
agent: { id },
|
|
1702
|
+
can: params.can,
|
|
1703
|
+
...(params.syncGroups ? { syncGroups: params.syncGroups } : {}),
|
|
1704
|
+
...(params.ttlSeconds !== undefined ? { ttlSeconds: params.ttlSeconds } : {}),
|
|
1705
|
+
...(userMeta ? { userMeta } : {}),
|
|
1706
|
+
};
|
|
1707
|
+
// Re-mint the `rk_` on every resolver call so a long-lived agent client
|
|
1708
|
+
// never hits token expiry; the `sk_` stays in THIS process — the child
|
|
1709
|
+
// only ever sees its own short-lived `rk_`.
|
|
1710
|
+
const mintToken = async () => (await mintSession(sessionParams, await buildMintContext('agents.create')))
|
|
1711
|
+
.token;
|
|
1712
|
+
// Mint once up front so a bad key / denied scope throws HERE, not later
|
|
1713
|
+
// inside the child's bootstrap; reuse that first token, re-mint on refresh.
|
|
1714
|
+
let pending = await mintToken();
|
|
1715
|
+
const apiKey = async () => {
|
|
1716
|
+
if (pending !== null) {
|
|
1717
|
+
const token = pending;
|
|
1718
|
+
pending = null;
|
|
1719
|
+
return token;
|
|
1720
|
+
}
|
|
1721
|
+
return mintToken();
|
|
1722
|
+
};
|
|
1723
|
+
return Ablo({ ...internalOptions, apiKey });
|
|
1634
1724
|
},
|
|
1635
1725
|
},
|
|
1636
1726
|
async dispose() {
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { AbloOptions, CommitResource, ClaimCreateOptions, ClaimWaitOptions, ModelClient, ModelClaim, ModelTarget, CreateSessionParams, AbloSession } from './Ablo.js';
|
|
9
9
|
import type { SchemaRecord } from '../schema/schema.js';
|
|
10
|
-
import type { ClaimHandle } from './createModelProxy.js';
|
|
11
10
|
import type { Duration } from '../utils/duration.js';
|
|
11
|
+
import type { Claim } from '../types/streams.js';
|
|
12
12
|
export type AbloApiClientOptions = Omit<AbloOptions, 'schema'> & {
|
|
13
13
|
readonly schema?: null | undefined;
|
|
14
14
|
readonly bootstrapBaseUrl?: string | undefined;
|
|
15
15
|
};
|
|
16
16
|
export interface AbloApiClaims {
|
|
17
|
-
create(options: ClaimCreateOptions): Promise<
|
|
17
|
+
create(options: ClaimCreateOptions): Promise<Claim>;
|
|
18
18
|
list(target?: Partial<ModelTarget>): Promise<readonly ModelClaim[]>;
|
|
19
19
|
waitFor(target: Partial<ModelTarget>, options?: ClaimWaitOptions): Promise<void>;
|
|
20
20
|
}
|