@abloatai/ablo 0.18.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 +37 -0
- 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 +253 -160
- package/dist/client/Ablo.d.ts +83 -22
- package/dist/client/Ablo.js +103 -30
- 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 +208 -0
- 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/errorCodes.d.ts +2 -0
- package/dist/errorCodes.js +2 -0
- package/dist/errors.d.ts +1 -2
- package/dist/errors.js +6 -9
- package/dist/index.d.ts +5 -1
- package/dist/index.js +7 -0
- package/dist/interfaces/index.d.ts +45 -2
- package/dist/policy/types.d.ts +18 -9
- package/dist/policy/types.js +26 -16
- 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/sync/HydrationCoordinator.js +7 -3
- package/dist/sync/SyncWebSocket.d.ts +11 -2
- package/dist/sync/SyncWebSocket.js +67 -3
- package/dist/sync/createClaimStream.d.ts +9 -2
- package/dist/sync/createClaimStream.js +9 -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/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
|
/**
|
|
@@ -403,7 +404,7 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
403
404
|
* `create({ data })` / `update({ id, data })` / `delete({ id })` — writes
|
|
404
405
|
* `claim({ id })` — durable claim handle for coordinated writes
|
|
405
406
|
*/
|
|
406
|
-
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';
|
|
407
408
|
import type { ModelOperations, ClaimOptions, ClaimParams, ClaimReadApi, AwaitedClaimMethod, ServerReadOptions } from './createModelProxy.js';
|
|
408
409
|
export type ModelOperationAction = 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
|
|
409
410
|
export type CommitWait = 'queued' | 'confirmed';
|
|
@@ -428,7 +429,7 @@ export interface ModelReadOptions extends ClaimedOptions {
|
|
|
428
429
|
export interface ClaimCreateOptions {
|
|
429
430
|
readonly target: ModelTarget;
|
|
430
431
|
/** Human-readable phase shown to peers — `'editing'`, `'writing'`. The same
|
|
431
|
-
* word on every claim surface
|
|
432
|
+
* word on every claim surface. */
|
|
432
433
|
readonly reason: string;
|
|
433
434
|
readonly ttl?: Duration;
|
|
434
435
|
/**
|
|
@@ -474,7 +475,7 @@ export interface CommitCreateOptions {
|
|
|
474
475
|
* against concurrent edits without re-stating the watermark by hand.
|
|
475
476
|
* Explicit `readAt`/`onStale` on the options win.
|
|
476
477
|
*/
|
|
477
|
-
readonly claim?:
|
|
478
|
+
readonly claim?: Claim<Record<string, unknown>> | null;
|
|
478
479
|
readonly operation?: CommitOperationInput;
|
|
479
480
|
readonly operations?: readonly CommitOperationInput[];
|
|
480
481
|
readonly wait?: CommitWait;
|
|
@@ -513,7 +514,7 @@ export interface CommitResource {
|
|
|
513
514
|
create(options: CommitCreateOptions): Promise<CommitReceipt>;
|
|
514
515
|
}
|
|
515
516
|
export interface ClaimResource extends ClaimStream {
|
|
516
|
-
create(options: ClaimCreateOptions): Promise<
|
|
517
|
+
create(options: ClaimCreateOptions): Promise<Claim>;
|
|
517
518
|
list(target?: Partial<ModelTarget>): readonly ModelClaim[];
|
|
518
519
|
waitFor(target: Partial<ModelTarget>, options?: ClaimWaitOptions): Promise<void>;
|
|
519
520
|
}
|
|
@@ -525,7 +526,7 @@ export interface ModelMutationOptions extends ClaimedOptions {
|
|
|
525
526
|
readonly readAt?: number | null;
|
|
526
527
|
readonly onStale?: 'reject' | 'overwrite' | 'notify' | null;
|
|
527
528
|
readonly wait?: CommitWait;
|
|
528
|
-
readonly claim?:
|
|
529
|
+
readonly claim?: Claim | ClaimOptions | null;
|
|
529
530
|
}
|
|
530
531
|
/**
|
|
531
532
|
* The HTTP/stateless claim surface. Normal tools usually put `claim` directly
|
|
@@ -543,7 +544,7 @@ export interface ModelMutationOptions extends ClaimedOptions {
|
|
|
543
544
|
* wrapper that statelessness forces. `claim({ id })` is identical (already async
|
|
544
545
|
* on both); `state`/`queue`/`reorder`/`release` are the awaited form.
|
|
545
546
|
*/
|
|
546
|
-
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>>) & {
|
|
547
548
|
[K in keyof ClaimReadApi<T>]: AwaitedClaimMethod<ClaimReadApi<T>[K]>;
|
|
548
549
|
};
|
|
549
550
|
export interface ModelClient<T = Record<string, unknown>> {
|
|
@@ -646,6 +647,38 @@ export interface CreateAgentSessionParams<S extends SchemaRecord> {
|
|
|
646
647
|
* `{ user }` for a full-authority end-user session (`ek_`) or `{ agent, can }`
|
|
647
648
|
* for a scoped agent session (`rk_`). */
|
|
648
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
|
+
}
|
|
649
682
|
/** A minted session token — the Stripe ephemeral-key / Supabase session
|
|
650
683
|
* resource. `token` is the secret the holder presents as its bearer. */
|
|
651
684
|
export interface AbloSession {
|
|
@@ -766,6 +799,35 @@ export type Ablo<S extends SchemaRecord> = {
|
|
|
766
799
|
sessions: {
|
|
767
800
|
create(params: CreateSessionParams<S>): Promise<AbloSession>;
|
|
768
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
|
+
};
|
|
769
831
|
/**
|
|
770
832
|
* The organization this client resolved to — `null` until `ready()`
|
|
771
833
|
* completes. Use it instead of scraping CLI output or hardcoding env vars:
|
|
@@ -1007,7 +1069,7 @@ export declare namespace Ablo {
|
|
|
1007
1069
|
type RotatedCapability = import('./ApiClient.js').RotatedCapability;
|
|
1008
1070
|
type IfClaimedPolicy = import('./Ablo.js').IfClaimedPolicy;
|
|
1009
1071
|
type ClaimedOptions = import('./Ablo.js').ClaimedOptions;
|
|
1010
|
-
type
|
|
1072
|
+
type ClaimTarget = _Streams.ClaimTarget;
|
|
1011
1073
|
type PresenceTarget = _Streams.PresenceTarget;
|
|
1012
1074
|
type TargetRange = _Streams.TargetRange;
|
|
1013
1075
|
type Duration = _Streams.Duration;
|
|
@@ -1015,8 +1077,7 @@ export declare namespace Ablo {
|
|
|
1015
1077
|
type ClaimStream = _Streams.ClaimStream;
|
|
1016
1078
|
type Peer = _Streams.Peer;
|
|
1017
1079
|
type Activity = _Streams.Activity;
|
|
1018
|
-
type
|
|
1019
|
-
type ClaimHandle = _Streams.ClaimHandle;
|
|
1080
|
+
type Claim = _Streams.Claim;
|
|
1020
1081
|
type ClaimRejection = _Streams.ClaimRejection;
|
|
1021
1082
|
type ClaimLost = _Streams.ClaimLost;
|
|
1022
1083
|
type Snapshot<TSchema extends _SchemaTypes.Schema = _SchemaTypes.Schema, K extends keyof TSchema['models'] = keyof TSchema['models']> = _Streams.Snapshot<TSchema, K>;
|
|
@@ -1053,7 +1114,7 @@ export declare namespace Ablo {
|
|
|
1053
1114
|
type Client = import('./Ablo.js').CommitResource;
|
|
1054
1115
|
}
|
|
1055
1116
|
namespace Claim {
|
|
1056
|
-
type Handle = import('./Ablo.js').
|
|
1117
|
+
type Handle = import('./Ablo.js').Claim;
|
|
1057
1118
|
type CreateOptions = import('./Ablo.js').ClaimCreateOptions;
|
|
1058
1119
|
type WaitOptions = import('./Ablo.js').ClaimWaitOptions;
|
|
1059
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';
|
|
@@ -746,6 +746,8 @@ export function Ablo(options) {
|
|
|
746
746
|
// Nudge (once) if a stray DATABASE_URL is in the env but `databaseUrl` wasn't
|
|
747
747
|
// passed — the env value is no longer auto-adopted (see resolveDatabaseUrl).
|
|
748
748
|
warnIfDatabaseUrlEnvIgnored(authInput, (m) => logger.warn(m));
|
|
749
|
+
warnIfDatabaseUrlDeprecated(authInput, (m) => logger.warn(m));
|
|
750
|
+
void warnIfCliKeyMismatch(authInput, (m) => logger.warn(m));
|
|
749
751
|
const schema = options.schema;
|
|
750
752
|
const url = resolveBaseURL(authInput);
|
|
751
753
|
// 1. Derive config from schema
|
|
@@ -894,6 +896,19 @@ export function Ablo(options) {
|
|
|
894
896
|
store.syncStatus.state = 'error';
|
|
895
897
|
store.syncStatus.error = _validationError;
|
|
896
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
|
+
}
|
|
897
912
|
// 7. The ready() promise drives the BaseSyncedStore.initialize() generator
|
|
898
913
|
// to completion. First call kicks off the initialization; subsequent
|
|
899
914
|
// calls return the same promise (idempotent).
|
|
@@ -1117,7 +1132,7 @@ export function Ablo(options) {
|
|
|
1117
1132
|
return (typeof value === 'object' &&
|
|
1118
1133
|
value !== null &&
|
|
1119
1134
|
value.object === 'claim' &&
|
|
1120
|
-
typeof value.
|
|
1135
|
+
typeof value.id === 'string');
|
|
1121
1136
|
}
|
|
1122
1137
|
function normalizeCommitOperation(op, defaults) {
|
|
1123
1138
|
const model = op.model ?? op.target?.model;
|
|
@@ -1152,13 +1167,13 @@ export function Ablo(options) {
|
|
|
1152
1167
|
const description = descriptionFromMeta(claim.target.meta);
|
|
1153
1168
|
return {
|
|
1154
1169
|
id: claim.id,
|
|
1155
|
-
actor: claim.heldBy,
|
|
1156
|
-
participantKind: claim.participantKind,
|
|
1170
|
+
actor: claim.heldBy ?? "",
|
|
1171
|
+
participantKind: claim.participantKind ?? "user",
|
|
1157
1172
|
reason: claim.reason,
|
|
1158
1173
|
...(description ? { description } : {}),
|
|
1159
1174
|
field: claim.target.field,
|
|
1160
1175
|
status: 'active',
|
|
1161
|
-
expiresAt: claim.expiresAt,
|
|
1176
|
+
expiresAt: claim.expiresAt ?? 0,
|
|
1162
1177
|
target: {
|
|
1163
1178
|
model: claim.target.type,
|
|
1164
1179
|
id: claim.target.id,
|
|
@@ -1172,14 +1187,14 @@ export function Ablo(options) {
|
|
|
1172
1187
|
function modelClaimFromQueued(claim) {
|
|
1173
1188
|
return {
|
|
1174
1189
|
id: claim.id,
|
|
1175
|
-
actor: claim.heldBy,
|
|
1176
|
-
participantKind: claim.participantKind,
|
|
1190
|
+
actor: claim.heldBy ?? "",
|
|
1191
|
+
participantKind: claim.participantKind ?? "user",
|
|
1177
1192
|
reason: claim.reason,
|
|
1178
1193
|
...(claim.description ? { description: claim.description } : {}),
|
|
1179
1194
|
field: claim.target.field,
|
|
1180
1195
|
status: 'queued',
|
|
1181
1196
|
position: claim.position,
|
|
1182
|
-
expiresAt: claim.expiresAt,
|
|
1197
|
+
expiresAt: claim.expiresAt ?? 0,
|
|
1183
1198
|
target: {
|
|
1184
1199
|
model: claim.target.type,
|
|
1185
1200
|
id: claim.target.id,
|
|
@@ -1271,11 +1286,11 @@ export function Ablo(options) {
|
|
|
1271
1286
|
}
|
|
1272
1287
|
function wrapClaimHandle(claim, waited = false) {
|
|
1273
1288
|
const release = async () => {
|
|
1274
|
-
claim.revoke();
|
|
1289
|
+
claim.revoke?.();
|
|
1275
1290
|
};
|
|
1276
1291
|
return {
|
|
1277
1292
|
object: 'claim',
|
|
1278
|
-
|
|
1293
|
+
id: claim.id,
|
|
1279
1294
|
reason: claim.reason,
|
|
1280
1295
|
target: claim.target,
|
|
1281
1296
|
waited,
|
|
@@ -1309,7 +1324,7 @@ export function Ablo(options) {
|
|
|
1309
1324
|
const ws = store.getSyncWebSocket();
|
|
1310
1325
|
if (ws) {
|
|
1311
1326
|
try {
|
|
1312
|
-
({ waited } = await awaitClaimGrant(ws, claim.
|
|
1327
|
+
({ waited } = await awaitClaimGrant(ws, claim.id, {
|
|
1313
1328
|
timeoutMs: claimOptions.waitTimeoutMs,
|
|
1314
1329
|
maxQueueDepth: claimOptions.maxQueueDepth,
|
|
1315
1330
|
}));
|
|
@@ -1318,7 +1333,7 @@ export function Ablo(options) {
|
|
|
1318
1333
|
// Gave up waiting (queue too deep, timed out, or lost) — abandon
|
|
1319
1334
|
// the queued claim so we don't leave a phantom entry in the
|
|
1320
1335
|
// line that would block or mislead other claimers.
|
|
1321
|
-
claim.revoke();
|
|
1336
|
+
claim.revoke?.();
|
|
1322
1337
|
throw err;
|
|
1323
1338
|
}
|
|
1324
1339
|
}
|
|
@@ -1430,7 +1445,7 @@ export function Ablo(options) {
|
|
|
1430
1445
|
onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
|
|
1431
1446
|
});
|
|
1432
1447
|
const wait = commitOptions.wait ?? 'confirmed';
|
|
1433
|
-
const claimId = normalizeClaimId(commitOptions.claimRef) ?? claim?.
|
|
1448
|
+
const claimId = normalizeClaimId(commitOptions.claimRef) ?? claim?.id;
|
|
1434
1449
|
void claimId; // The current wire clears claims by entity after commit.
|
|
1435
1450
|
// Route through the TransactionQueue's commit lane so the call
|
|
1436
1451
|
// tolerates WS disconnects: the envelope stays in memory until
|
|
@@ -1579,6 +1594,34 @@ export function Ablo(options) {
|
|
|
1579
1594
|
async function controlPlaneApiKey() {
|
|
1580
1595
|
return resolveApiKeyValue(configuredApiKey);
|
|
1581
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
|
+
}
|
|
1582
1625
|
const engine = {
|
|
1583
1626
|
...modelProxies,
|
|
1584
1627
|
ready,
|
|
@@ -1631,23 +1674,53 @@ export function Ablo(options) {
|
|
|
1631
1674
|
// agent credential silently replacing the secret key on control-plane
|
|
1632
1675
|
// calls is how humans get minted as agents — attribution is the product.
|
|
1633
1676
|
async create(params) {
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
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 });
|
|
1651
1724
|
},
|
|
1652
1725
|
},
|
|
1653
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
|
}
|
package/dist/client/ApiClient.js
CHANGED
|
@@ -6,23 +6,11 @@
|
|
|
6
6
|
* nouns directly to HTTP routes on sync-server.
|
|
7
7
|
*/
|
|
8
8
|
import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, claimedError, translateHttpError, } from '../errors.js';
|
|
9
|
-
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfDatabaseUrlEnvIgnored, } from './auth.js';
|
|
9
|
+
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfCliKeyMismatch, warnIfDatabaseUrlEnvIgnored, warnIfDatabaseUrlDeprecated, } from './auth.js';
|
|
10
10
|
import { registerDataSource } from './registerDataSource.js';
|
|
11
11
|
import { toSeconds } from '../utils/duration.js';
|
|
12
12
|
import { mintSession } from './sessionMint.js';
|
|
13
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
|
-
}
|
|
26
14
|
const DEFAULT_AGENT_LEASE = '10m';
|
|
27
15
|
export function createProtocolClient(options) {
|
|
28
16
|
const env = readProcessEnv();
|
|
@@ -33,6 +21,8 @@ export function createProtocolClient(options) {
|
|
|
33
21
|
// Nudge (once) if a stray DATABASE_URL is in the env but `databaseUrl` wasn't
|
|
34
22
|
// passed — no logger on this path, so the helper falls back to console.warn.
|
|
35
23
|
warnIfDatabaseUrlEnvIgnored(authInput);
|
|
24
|
+
warnIfDatabaseUrlDeprecated(authInput);
|
|
25
|
+
void warnIfCliKeyMismatch(authInput);
|
|
36
26
|
assertBrowserSafety({
|
|
37
27
|
apiKey: configuredApiKey,
|
|
38
28
|
databaseUrl: configuredDatabaseUrl,
|
|
@@ -185,8 +175,8 @@ export function createProtocolClient(options) {
|
|
|
185
175
|
const suffix = params.toString();
|
|
186
176
|
const body = await requestJson(`/v1/claims${suffix ? `?${suffix}` : ''}`, { method: 'GET' });
|
|
187
177
|
return {
|
|
188
|
-
active:
|
|
189
|
-
queue:
|
|
178
|
+
active: body.claims ?? [],
|
|
179
|
+
queue: body.queue ?? [],
|
|
190
180
|
};
|
|
191
181
|
}
|
|
192
182
|
function delay(ms, signal) {
|
|
@@ -272,7 +262,7 @@ export function createProtocolClient(options) {
|
|
|
272
262
|
body: JSON.stringify({
|
|
273
263
|
clientTxId,
|
|
274
264
|
idempotencyKey: clientTxId,
|
|
275
|
-
claim: normalizeClaimId(commitOptions.claimRef) ?? claim?.
|
|
265
|
+
claim: normalizeClaimId(commitOptions.claimRef) ?? claim?.id,
|
|
276
266
|
operations,
|
|
277
267
|
}),
|
|
278
268
|
});
|
|
@@ -389,8 +379,7 @@ export function createProtocolClient(options) {
|
|
|
389
379
|
body: JSON.stringify({
|
|
390
380
|
claimId,
|
|
391
381
|
target: claimOptions.target,
|
|
392
|
-
|
|
393
|
-
action: claimOptions.reason,
|
|
382
|
+
reason: claimOptions.reason,
|
|
394
383
|
ttl: claimOptions.ttl,
|
|
395
384
|
queue: claimOptions.queue,
|
|
396
385
|
}),
|
|
@@ -415,9 +404,16 @@ export function createProtocolClient(options) {
|
|
|
415
404
|
};
|
|
416
405
|
return {
|
|
417
406
|
object: 'claim',
|
|
418
|
-
|
|
407
|
+
id,
|
|
419
408
|
reason: claimOptions.reason,
|
|
420
|
-
target:
|
|
409
|
+
target: {
|
|
410
|
+
type: claimOptions.target.model,
|
|
411
|
+
id: claimOptions.target.id,
|
|
412
|
+
...(claimOptions.target.field ? { field: claimOptions.target.field } : {}),
|
|
413
|
+
...(claimOptions.target.path ? { path: claimOptions.target.path } : {}),
|
|
414
|
+
...(claimOptions.target.range ? { range: claimOptions.target.range } : {}),
|
|
415
|
+
...(claimOptions.target.meta ? { meta: claimOptions.target.meta } : {}),
|
|
416
|
+
},
|
|
421
417
|
release,
|
|
422
418
|
revoke: () => {
|
|
423
419
|
void release().catch(() => { });
|
|
@@ -467,7 +463,7 @@ export function createProtocolClient(options) {
|
|
|
467
463
|
return {
|
|
468
464
|
data,
|
|
469
465
|
stamp: query.stamp ?? 0,
|
|
470
|
-
claims:
|
|
466
|
+
claims: query.claims ?? [],
|
|
471
467
|
};
|
|
472
468
|
}
|
|
473
469
|
/**
|
|
@@ -501,13 +497,13 @@ export function createProtocolClient(options) {
|
|
|
501
497
|
const claimHandle = typeof options?.claim === 'object' &&
|
|
502
498
|
options?.claim !== null &&
|
|
503
499
|
options.claim.object === 'claim' &&
|
|
504
|
-
typeof options.claim.
|
|
500
|
+
typeof options.claim.id === 'string'
|
|
505
501
|
? options.claim
|
|
506
502
|
: undefined;
|
|
507
503
|
const readAt = options?.readAt ?? claimHandle?.readAt;
|
|
508
504
|
const requestBody = {
|
|
509
505
|
idempotencyKey: clientTxId,
|
|
510
|
-
claim: normalizeClaimId(options?.claimRef) ?? claimHandle?.
|
|
506
|
+
claim: normalizeClaimId(options?.claimRef) ?? claimHandle?.id,
|
|
511
507
|
onStale: options?.onStale ?? (claimHandle?.readAt !== undefined ? 'reject' : undefined),
|
|
512
508
|
readAt,
|
|
513
509
|
};
|
|
@@ -538,7 +534,7 @@ export function createProtocolClient(options) {
|
|
|
538
534
|
const isClaimHandle = (value) => typeof value === 'object' &&
|
|
539
535
|
value !== null &&
|
|
540
536
|
value.object === 'claim' &&
|
|
541
|
-
typeof value.
|
|
537
|
+
typeof value.id === 'string' &&
|
|
542
538
|
typeof value.release === 'function';
|
|
543
539
|
const claimMeta = (options) => {
|
|
544
540
|
if (!options?.description)
|
|
@@ -549,8 +545,7 @@ export function createProtocolClient(options) {
|
|
|
549
545
|
const body = await requestJson(claimPath(params.id), {
|
|
550
546
|
method: 'POST',
|
|
551
547
|
body: JSON.stringify({
|
|
552
|
-
|
|
553
|
-
action: params.reason ?? 'editing',
|
|
548
|
+
reason: params.reason ?? 'editing',
|
|
554
549
|
...(params.ttl !== undefined ? { ttl: params.ttl } : {}),
|
|
555
550
|
...(params.description !== undefined ? { description: params.description } : {}),
|
|
556
551
|
...(claimMeta(params) ? { meta: claimMeta(params) } : {}),
|
|
@@ -563,7 +558,7 @@ export function createProtocolClient(options) {
|
|
|
563
558
|
throw new AbloClaimedError(`Target ${name}/${params.id} is held; queued at position ${body.position ?? 0}. ` +
|
|
564
559
|
`The HTTP client cannot await the grant without a WebSocket.`, { code: 'claim_queued' });
|
|
565
560
|
}
|
|
566
|
-
return body.claim?.id ?? body.id ?? body.
|
|
561
|
+
return body.claim?.id ?? body.id ?? body.id ?? createClaimId();
|
|
567
562
|
};
|
|
568
563
|
const releaseClaim = (params) => requestJson(claimPath(isClaimHandle(params) ? params.target.id : params.id), { method: 'DELETE' }).then(() => undefined);
|
|
569
564
|
async function claimImpl(params) {
|
|
@@ -572,10 +567,10 @@ export function createProtocolClient(options) {
|
|
|
572
567
|
const release = () => releaseClaim(params);
|
|
573
568
|
return {
|
|
574
569
|
object: 'claim',
|
|
575
|
-
claimId,
|
|
570
|
+
id: claimId,
|
|
576
571
|
readAt: stamp,
|
|
577
572
|
target: {
|
|
578
|
-
|
|
573
|
+
type: name,
|
|
579
574
|
id: params.id,
|
|
580
575
|
...(params.field ? { field: params.field } : {}),
|
|
581
576
|
...(params.path ? { path: params.path } : {}),
|
|
@@ -598,11 +593,11 @@ export function createProtocolClient(options) {
|
|
|
598
593
|
state: async (params) => {
|
|
599
594
|
const res = await claimsForEntity(params);
|
|
600
595
|
const first = res.claims?.[0];
|
|
601
|
-
return first
|
|
596
|
+
return first ?? null;
|
|
602
597
|
},
|
|
603
598
|
queue: async (params) => {
|
|
604
599
|
const res = await claimsForEntity(params);
|
|
605
|
-
return { object: 'list', data:
|
|
600
|
+
return { object: 'list', data: res.queue ?? [] };
|
|
606
601
|
},
|
|
607
602
|
reorder: async (params) => {
|
|
608
603
|
await requestJson(`${claimPath(params.id)}/reorder`, {
|
|
@@ -618,7 +613,7 @@ export function createProtocolClient(options) {
|
|
|
618
613
|
if (!claimInput)
|
|
619
614
|
return run(input);
|
|
620
615
|
if (isClaimHandle(claimInput)) {
|
|
621
|
-
return run({ ...input, claimRef: { id: claimInput.
|
|
616
|
+
return run({ ...input, claimRef: { id: claimInput.id }, claim: undefined });
|
|
622
617
|
}
|
|
623
618
|
const claimId = await acquireClaim({ id, ...claimInput });
|
|
624
619
|
try {
|
package/dist/client/auth.d.ts
CHANGED
|
@@ -49,6 +49,29 @@ export interface AuthResolveInput {
|
|
|
49
49
|
export declare function readProcessEnv(): Record<string, string | undefined>;
|
|
50
50
|
export declare function resolveApiKey(input: AuthResolveInput): string | ApiKeySetter | null;
|
|
51
51
|
export declare function resolveAuthToken(input: AuthResolveInput): string | null;
|
|
52
|
+
type CliMode = 'sandbox' | 'production';
|
|
53
|
+
type StaticApiKeySource = 'option' | 'env';
|
|
54
|
+
interface StaticApiKey {
|
|
55
|
+
readonly key: string;
|
|
56
|
+
readonly source: StaticApiKeySource;
|
|
57
|
+
}
|
|
58
|
+
interface CliCredentialSnapshot {
|
|
59
|
+
readonly mode: CliMode;
|
|
60
|
+
readonly activeProfile: string;
|
|
61
|
+
readonly storedKey?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface CliKeyMismatch {
|
|
64
|
+
readonly source: StaticApiKeySource;
|
|
65
|
+
readonly configuredKeyPrefix: string;
|
|
66
|
+
readonly configuredMode?: CliMode;
|
|
67
|
+
readonly cliMode: CliMode;
|
|
68
|
+
readonly storedKeyPrefix?: string;
|
|
69
|
+
readonly kind: 'mode_mismatch' | 'key_override';
|
|
70
|
+
readonly message: string;
|
|
71
|
+
}
|
|
72
|
+
/** Infer sandbox/production from Ablo key prefixes without importing CLI code. */
|
|
73
|
+
export declare function modeFromApiKey(key: string): CliMode | undefined;
|
|
74
|
+
export declare function describeCliKeyMismatch(configured: StaticApiKey, cli: CliCredentialSnapshot): CliKeyMismatch | null;
|
|
52
75
|
/**
|
|
53
76
|
* Resolve the direct-URL connector's Postgres connection string.
|
|
54
77
|
*
|
|
@@ -63,6 +86,8 @@ export declare function resolveAuthToken(input: AuthResolveInput): string | null
|
|
|
63
86
|
*/
|
|
64
87
|
export declare function resolveDatabaseUrl(input: AuthResolveInput): string | null;
|
|
65
88
|
export declare function warnIfDatabaseUrlEnvIgnored(input: AuthResolveInput, warn?: (message: string) => void): void;
|
|
89
|
+
export declare function warnIfDatabaseUrlDeprecated(input: AuthResolveInput, warn?: (message: string) => void): void;
|
|
90
|
+
export declare function warnIfCliKeyMismatch(input: AuthResolveInput, warn?: (message: string) => void): Promise<void>;
|
|
66
91
|
export declare const ABLO_HOSTED_API_DOMAIN = "api.abloatai.com";
|
|
67
92
|
export declare const ABLO_HOSTED_HTTP_BASE_URL = "https://api.abloatai.com";
|
|
68
93
|
export declare const ABLO_DEFAULT_BASE_URL = "https://api.abloatai.com";
|
|
@@ -108,3 +133,4 @@ export declare function resolveBootstrapBaseUrl(input: {
|
|
|
108
133
|
readonly url: string;
|
|
109
134
|
readonly bootstrapBaseUrl?: string;
|
|
110
135
|
}): string;
|
|
136
|
+
export {};
|