@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/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 {};
|
package/dist/client/auth.js
CHANGED
|
@@ -28,6 +28,160 @@ export function resolveApiKey(input) {
|
|
|
28
28
|
export function resolveAuthToken(input) {
|
|
29
29
|
return input.options.authToken ?? null;
|
|
30
30
|
}
|
|
31
|
+
function keyPrefix(key) {
|
|
32
|
+
return `${key.slice(0, 12)}…`;
|
|
33
|
+
}
|
|
34
|
+
/** Infer sandbox/production from Ablo key prefixes without importing CLI code. */
|
|
35
|
+
export function modeFromApiKey(key) {
|
|
36
|
+
if (/^(sk|rk)_test_/.test(key))
|
|
37
|
+
return 'sandbox';
|
|
38
|
+
if (/^(sk|rk)_live_/.test(key))
|
|
39
|
+
return 'production';
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
function resolveStaticApiKey(input) {
|
|
43
|
+
if (typeof input.options.apiKey === 'string') {
|
|
44
|
+
return { key: input.options.apiKey, source: 'option' };
|
|
45
|
+
}
|
|
46
|
+
if (input.options.apiKey !== undefined && input.options.apiKey !== null) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const envKey = input.env.ABLO_API_KEY;
|
|
50
|
+
if (typeof envKey === 'string' && envKey.length > 0) {
|
|
51
|
+
return { key: envKey, source: 'env' };
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function readProfileKeys(value) {
|
|
56
|
+
if (!value || typeof value !== 'object')
|
|
57
|
+
return {};
|
|
58
|
+
const profiles = {};
|
|
59
|
+
for (const [name, raw] of Object.entries(value)) {
|
|
60
|
+
if (!raw || typeof raw !== 'object')
|
|
61
|
+
continue;
|
|
62
|
+
const row = raw;
|
|
63
|
+
const sandbox = row.sandbox;
|
|
64
|
+
const production = row.production;
|
|
65
|
+
profiles[name] = {
|
|
66
|
+
sandbox: sandbox && typeof sandbox === 'object' ? sandbox : undefined,
|
|
67
|
+
production: production && typeof production === 'object' ? production : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return profiles;
|
|
71
|
+
}
|
|
72
|
+
function legacyProfileKeys(value) {
|
|
73
|
+
if (!value)
|
|
74
|
+
return { sandbox: undefined, production: undefined };
|
|
75
|
+
const sandbox = value.sandbox;
|
|
76
|
+
const production = value.production;
|
|
77
|
+
if ((sandbox && typeof sandbox === 'object') ||
|
|
78
|
+
(production && typeof production === 'object')) {
|
|
79
|
+
return {
|
|
80
|
+
sandbox: sandbox && typeof sandbox === 'object' ? sandbox : undefined,
|
|
81
|
+
production: production && typeof production === 'object' ? production : undefined,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (typeof value.apiKey === 'string') {
|
|
85
|
+
return { sandbox: { apiKey: value.apiKey }, production: undefined };
|
|
86
|
+
}
|
|
87
|
+
return { sandbox: undefined, production: undefined };
|
|
88
|
+
}
|
|
89
|
+
function normalizeCliMode(value) {
|
|
90
|
+
return value === 'sandbox' || value === 'production' ? value : undefined;
|
|
91
|
+
}
|
|
92
|
+
function activeProjectSlug(value) {
|
|
93
|
+
if (!value || typeof value !== 'object')
|
|
94
|
+
return undefined;
|
|
95
|
+
const slug = value.slug;
|
|
96
|
+
return typeof slug === 'string' && slug.length > 0 ? slug : undefined;
|
|
97
|
+
}
|
|
98
|
+
function importNodeBuiltin(specifier) {
|
|
99
|
+
return import(specifier);
|
|
100
|
+
}
|
|
101
|
+
async function readJsonIfPresent(path) {
|
|
102
|
+
try {
|
|
103
|
+
const { readFile } = await importNodeBuiltin('node:fs/promises');
|
|
104
|
+
const text = await readFile(path, 'utf8');
|
|
105
|
+
const parsed = JSON.parse(text);
|
|
106
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function readCliCredentialSnapshot(env) {
|
|
113
|
+
const processLike = globalThis.process;
|
|
114
|
+
if (!processLike?.versions?.node)
|
|
115
|
+
return null;
|
|
116
|
+
if (typeof window !== 'undefined')
|
|
117
|
+
return null;
|
|
118
|
+
const [{ homedir }, { join }] = await Promise.all([
|
|
119
|
+
importNodeBuiltin('node:os'),
|
|
120
|
+
importNodeBuiltin('node:path'),
|
|
121
|
+
]);
|
|
122
|
+
const dir = env.ABLO_CONFIG_DIR
|
|
123
|
+
?? (env.XDG_CONFIG_HOME ? join(env.XDG_CONFIG_HOME, 'ablo') : join(homedir(), '.config', 'ablo'));
|
|
124
|
+
const [cfg, creds] = await Promise.all([
|
|
125
|
+
readJsonIfPresent(join(dir, 'config.json')),
|
|
126
|
+
readJsonIfPresent(join(dir, 'credentials.json')),
|
|
127
|
+
]);
|
|
128
|
+
const mode = normalizeCliMode(cfg?.mode) ?? normalizeCliMode(creds?.mode);
|
|
129
|
+
const activeProfile = activeProjectSlug(cfg?.activeProject) ?? 'default';
|
|
130
|
+
const profiles = {
|
|
131
|
+
...readProfileKeys(creds?.profiles),
|
|
132
|
+
...readProfileKeys(cfg?.profiles),
|
|
133
|
+
};
|
|
134
|
+
if (!profiles[activeProfile]) {
|
|
135
|
+
const legacy = { ...legacyProfileKeys(cfg), ...legacyProfileKeys(creds) };
|
|
136
|
+
if (legacy.sandbox?.apiKey || legacy.production?.apiKey) {
|
|
137
|
+
profiles[activeProfile] = legacy;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const effectiveMode = mode ?? (Object.keys(profiles).length > 0 ? 'sandbox' : undefined);
|
|
141
|
+
if (!effectiveMode)
|
|
142
|
+
return null;
|
|
143
|
+
return {
|
|
144
|
+
mode: effectiveMode,
|
|
145
|
+
activeProfile,
|
|
146
|
+
...(profiles[activeProfile]?.[effectiveMode]?.apiKey
|
|
147
|
+
? { storedKey: profiles[activeProfile][effectiveMode].apiKey }
|
|
148
|
+
: {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export function describeCliKeyMismatch(configured, cli) {
|
|
152
|
+
const configuredMode = modeFromApiKey(configured.key);
|
|
153
|
+
const configuredKeyPrefix = keyPrefix(configured.key);
|
|
154
|
+
const storedKeyPrefix = cli.storedKey ? keyPrefix(cli.storedKey) : undefined;
|
|
155
|
+
const sourceLabel = configured.source === 'env' ? 'ABLO_API_KEY' : 'configured apiKey';
|
|
156
|
+
if (configuredMode && configuredMode !== cli.mode) {
|
|
157
|
+
return {
|
|
158
|
+
source: configured.source,
|
|
159
|
+
configuredKeyPrefix,
|
|
160
|
+
configuredMode,
|
|
161
|
+
cliMode: cli.mode,
|
|
162
|
+
...(storedKeyPrefix ? { storedKeyPrefix } : {}),
|
|
163
|
+
kind: 'mode_mismatch',
|
|
164
|
+
message: `${sourceLabel} is a ${configuredMode} key (${configuredKeyPrefix}) but the Ablo CLI is in ` +
|
|
165
|
+
`${cli.mode} mode${storedKeyPrefix ? ` (active stored key ${storedKeyPrefix})` : ''}. ` +
|
|
166
|
+
`Requests will use ${configuredMode}. Use the ${cli.mode} key, unset ABLO_API_KEY, ` +
|
|
167
|
+
`or run \`ablo mode ${configuredMode}\` intentionally.`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (configured.source === 'env' && cli.storedKey && configured.key !== cli.storedKey) {
|
|
171
|
+
return {
|
|
172
|
+
source: configured.source,
|
|
173
|
+
configuredKeyPrefix,
|
|
174
|
+
...(configuredMode ? { configuredMode } : {}),
|
|
175
|
+
cliMode: cli.mode,
|
|
176
|
+
storedKeyPrefix,
|
|
177
|
+
kind: 'key_override',
|
|
178
|
+
message: `ABLO_API_KEY (${configuredKeyPrefix}) overrides the CLI's stored active ${cli.mode} key ` +
|
|
179
|
+
`(${storedKeyPrefix}). Requests will use the environment key. Unset ABLO_API_KEY to use ` +
|
|
180
|
+
'`ablo status` / `ablo mode` credentials.',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
31
185
|
/**
|
|
32
186
|
* Resolve the direct-URL connector's Postgres connection string.
|
|
33
187
|
*
|
|
@@ -83,7 +237,61 @@ export function warnIfDatabaseUrlEnvIgnored(input, warn) {
|
|
|
83
237
|
if (warn)
|
|
84
238
|
warn(message);
|
|
85
239
|
else if (typeof console !== 'undefined')
|
|
86
|
-
console.warn('[
|
|
240
|
+
console.warn('[Ablo]', message);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* One-time deprecation nudge for the `databaseUrl` direct connector.
|
|
244
|
+
*
|
|
245
|
+
* `databaseUrl` registers the `dedicated` storage mode — Ablo opens a pool INTO
|
|
246
|
+
* the caller's Postgres and writes into it directly. That is the operate-their-
|
|
247
|
+
* database posture we are moving off. Ablo is Stripe-shaped: it hosts only the
|
|
248
|
+
* transaction log (the ordered sync_deltas) + coordination, never your data — your
|
|
249
|
+
* rows always live in your own database. The supported path is the signed Data
|
|
250
|
+
* Source endpoint (`dataSource(...)`), where your app owns the write and your
|
|
251
|
+
* credentials never leave it. See docs/plans/stripe-shaped-storage-posture.md.
|
|
252
|
+
*
|
|
253
|
+
* Still honored at runtime so existing integrations keep working; this only warns
|
|
254
|
+
* once per process (so it never spams) and falls back to `console.warn` when no
|
|
255
|
+
* logger is supplied (the `transport: 'http'`/`'api'` client has none).
|
|
256
|
+
*/
|
|
257
|
+
let warnedDatabaseUrlDeprecated = false;
|
|
258
|
+
export function warnIfDatabaseUrlDeprecated(input, warn) {
|
|
259
|
+
if (warnedDatabaseUrlDeprecated)
|
|
260
|
+
return;
|
|
261
|
+
if (input.options.databaseUrl == null)
|
|
262
|
+
return;
|
|
263
|
+
warnedDatabaseUrlDeprecated = true;
|
|
264
|
+
const message = '`databaseUrl` (the direct connector) is deprecated and will be removed from ' +
|
|
265
|
+
'the supported path. It lets Ablo dial into your database; we are moving off ' +
|
|
266
|
+
'that. Ablo hosts only the transaction log — your data stays in your DB. Expose ' +
|
|
267
|
+
'a signed Data Source endpoint (`dataSource(...)`) so your app owns the write, ' +
|
|
268
|
+
'or self-host the engine to keep the log in your infra too. ' +
|
|
269
|
+
'See docs/plans/stripe-shaped-storage-posture.md.';
|
|
270
|
+
if (warn)
|
|
271
|
+
warn(message);
|
|
272
|
+
else if (typeof console !== 'undefined')
|
|
273
|
+
console.warn('[Ablo]', message);
|
|
274
|
+
}
|
|
275
|
+
let warnedCliKeyMismatch = false;
|
|
276
|
+
export async function warnIfCliKeyMismatch(input, warn) {
|
|
277
|
+
if (warnedCliKeyMismatch)
|
|
278
|
+
return;
|
|
279
|
+
if (input.env.NODE_ENV === 'production')
|
|
280
|
+
return;
|
|
281
|
+
const configured = resolveStaticApiKey(input);
|
|
282
|
+
if (!configured)
|
|
283
|
+
return;
|
|
284
|
+
const cli = await readCliCredentialSnapshot(input.env);
|
|
285
|
+
if (!cli)
|
|
286
|
+
return;
|
|
287
|
+
const mismatch = describeCliKeyMismatch(configured, cli);
|
|
288
|
+
if (!mismatch)
|
|
289
|
+
return;
|
|
290
|
+
warnedCliKeyMismatch = true;
|
|
291
|
+
if (warn)
|
|
292
|
+
warn(mismatch.message);
|
|
293
|
+
else if (typeof console !== 'undefined')
|
|
294
|
+
console.warn('[Ablo]', mismatch.message);
|
|
87
295
|
}
|
|
88
296
|
export const ABLO_HOSTED_API_DOMAIN = 'api.abloatai.com';
|
|
89
297
|
export const ABLO_HOSTED_HTTP_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
|
|
@@ -22,7 +22,7 @@ import type { HydrationCoordinator } from '../sync/HydrationCoordinator.js';
|
|
|
22
22
|
import type { JoinedParticipant } from '../sync/participants.js';
|
|
23
23
|
import type { LoadWhere } from '../query/types.js';
|
|
24
24
|
import { ModelScope } from '../types/index.js';
|
|
25
|
-
import type { Duration, Claim,
|
|
25
|
+
import type { Duration, Claim, ClaimWaitOptions, Snapshot, TargetRange } from '../types/streams.js';
|
|
26
26
|
export interface ModelClientMeta {
|
|
27
27
|
readonly key: string;
|
|
28
28
|
readonly typename: string;
|
|
@@ -90,7 +90,7 @@ export interface ModelCollaboration<T> {
|
|
|
90
90
|
range?: TargetRange;
|
|
91
91
|
meta?: Record<string, unknown>;
|
|
92
92
|
};
|
|
93
|
-
/** Human-readable phase (`'editing'`)
|
|
93
|
+
/** Human-readable phase (`'editing'`). */
|
|
94
94
|
reason: string;
|
|
95
95
|
ttl?: Duration;
|
|
96
96
|
/**
|
|
@@ -101,7 +101,7 @@ export interface ModelCollaboration<T> {
|
|
|
101
101
|
queue?: boolean;
|
|
102
102
|
/** Reject (don't wait) if the queue is already this deep when we join. */
|
|
103
103
|
maxQueueDepth?: number;
|
|
104
|
-
}): Promise<
|
|
104
|
+
}): Promise<Claim>;
|
|
105
105
|
createSnapshot(modelKey: string, id: string): Snapshot;
|
|
106
106
|
/**
|
|
107
107
|
* Current coordination state on a target — who (if anyone) holds it.
|
|
@@ -185,7 +185,7 @@ export interface ModelCollaboration<T> {
|
|
|
185
185
|
}
|
|
186
186
|
export interface ClaimTargetOptions<T = Record<string, unknown>> {
|
|
187
187
|
/** Human-readable phase shown to observers while held. Defaults to
|
|
188
|
-
* `'editing'`. The same word on every claim surface
|
|
188
|
+
* `'editing'`. The same word on every claim surface. */
|
|
189
189
|
reason?: string;
|
|
190
190
|
/** Peer-visible explanation of the work being performed. */
|
|
191
191
|
description?: string;
|
|
@@ -258,7 +258,7 @@ export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLo
|
|
|
258
258
|
* `ablo.<model>.update({ id, data, claim })` verb — the handle carries the
|
|
259
259
|
* lease id and snapshot watermark for attribution + stale protection.
|
|
260
260
|
*/
|
|
261
|
-
export type {
|
|
261
|
+
export type { Claim };
|
|
262
262
|
export type ClaimOptions<T = Record<string, unknown>> = ClaimTargetOptions<T>;
|
|
263
263
|
/**
|
|
264
264
|
* The coordination surface for a model, exposed as a callable namespace.
|
|
@@ -312,7 +312,7 @@ export interface ClaimReadApi<T = Record<string, unknown>> {
|
|
|
312
312
|
*/
|
|
313
313
|
reorder(params: ClaimReorderParams<T>): void;
|
|
314
314
|
/** Release a manual claim handle early. Single-write claims auto-release. */
|
|
315
|
-
release(params: ClaimLookupParams<T> |
|
|
315
|
+
release(params: ClaimLookupParams<T> | Claim<T>): Promise<void>;
|
|
316
316
|
}
|
|
317
317
|
/**
|
|
318
318
|
* The awaited form of a claim method: a synchronous return becomes a `Promise`,
|
|
@@ -321,8 +321,13 @@ export interface ClaimReadApi<T = Record<string, unknown>> {
|
|
|
321
321
|
*/
|
|
322
322
|
export type AwaitedClaimMethod<F> = F extends (...args: infer A) => infer R ? R extends Promise<unknown> ? (...args: A) => R : (...args: A) => Promise<R> : F;
|
|
323
323
|
export interface ClaimApi<T> extends ClaimReadApi<T> {
|
|
324
|
-
/**
|
|
325
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Take a claim and get an explicit held-work handle back. Returns a
|
|
326
|
+
* {@link Claim} — `data` (and `readAt`) are guaranteed present
|
|
327
|
+
* because this door always re-reads the row under the lease, so callers use
|
|
328
|
+
* `handle.data` directly without a guard.
|
|
329
|
+
*/
|
|
330
|
+
(params: ClaimParams<T>): Promise<Claim<T>>;
|
|
326
331
|
}
|
|
327
332
|
export interface ModelRetrieveParams extends ServerRetrieveOptions {
|
|
328
333
|
readonly id: string;
|
|
@@ -330,16 +335,16 @@ export interface ModelRetrieveParams extends ServerRetrieveOptions {
|
|
|
330
335
|
export interface ModelCreateParams<T, CreateInput> extends MutationOptions {
|
|
331
336
|
readonly data: CreateInput;
|
|
332
337
|
readonly id?: string | null;
|
|
333
|
-
readonly claim?:
|
|
338
|
+
readonly claim?: Claim<T> | ClaimTargetOptions<T> | null;
|
|
334
339
|
}
|
|
335
340
|
export interface ModelUpdateParams<T> extends MutationOptions {
|
|
336
341
|
readonly id: string;
|
|
337
342
|
readonly data: Partial<T>;
|
|
338
|
-
readonly claim?:
|
|
343
|
+
readonly claim?: Claim<T> | ClaimTargetOptions<T> | null;
|
|
339
344
|
}
|
|
340
345
|
export interface ModelDeleteParams<T> extends MutationOptions {
|
|
341
346
|
readonly id: string;
|
|
342
|
-
readonly claim?:
|
|
347
|
+
readonly claim?: Claim<T> | ClaimTargetOptions<T> | null;
|
|
343
348
|
}
|
|
344
349
|
/** Options for the WebSocket-only `ablo.<model>.watch(ids, options?)`. */
|
|
345
350
|
export interface WatchOptions {
|
|
@@ -39,7 +39,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
39
39
|
// with the commit op's model name; a lease recorded under the schema key
|
|
40
40
|
// never collides with it — which silently disarmed the guard for every
|
|
41
41
|
// model whose schema key differs from its typename (i.e. nearly all of
|
|
42
|
-
// them, plural key vs singular typename). Public surfaces (
|
|
42
|
+
// them, plural key vs singular typename). Public surfaces (Claim.
|
|
43
43
|
// target.model) keep the schema key; only the wire/coordination targets
|
|
44
44
|
// use this.
|
|
45
45
|
const wireModel = registeredModelName.toLowerCase();
|
|
@@ -90,7 +90,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
90
90
|
const isClaimHandle = (value) => typeof value === 'object' &&
|
|
91
91
|
value !== null &&
|
|
92
92
|
value.object === 'claim' &&
|
|
93
|
-
typeof value.
|
|
93
|
+
typeof value.id === 'string' &&
|
|
94
94
|
typeof value.release === 'function';
|
|
95
95
|
const claimMeta = (options) => {
|
|
96
96
|
if (!options?.description)
|
|
@@ -131,7 +131,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
131
131
|
if (!held)
|
|
132
132
|
return;
|
|
133
133
|
activeClaims.delete(id);
|
|
134
|
-
await held.lease.release();
|
|
134
|
+
await held.lease.release?.();
|
|
135
135
|
};
|
|
136
136
|
const takeClaim = async (params) => {
|
|
137
137
|
if (!collaboration) {
|
|
@@ -214,7 +214,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
214
214
|
}
|
|
215
215
|
const snapshot = collaboration.createSnapshot(schemaKey, id);
|
|
216
216
|
const reason = options?.reason ?? 'editing';
|
|
217
|
-
// The self-claim's `
|
|
217
|
+
// The self-claim's `ClaimTarget` mirrors what a peer's `claim.state` would
|
|
218
218
|
// report (`state` maps `held.target.model` → `type`), so a holder and a
|
|
219
219
|
// peer see the SAME target.type for one row — the wire model token.
|
|
220
220
|
const selfTarget = {
|
|
@@ -235,7 +235,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
235
235
|
expiresAt,
|
|
236
236
|
});
|
|
237
237
|
const target = {
|
|
238
|
-
|
|
238
|
+
type: schemaKey,
|
|
239
239
|
id,
|
|
240
240
|
...(options?.field ? { field: options.field } : {}),
|
|
241
241
|
...(options?.path ? { path: options.path } : {}),
|
|
@@ -245,7 +245,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
245
245
|
const release = () => releaseClaim(id);
|
|
246
246
|
return {
|
|
247
247
|
object: 'claim',
|
|
248
|
-
|
|
248
|
+
id: lease.id,
|
|
249
249
|
readAt: snapshot.stamp,
|
|
250
250
|
target,
|
|
251
251
|
reason,
|
|
@@ -278,7 +278,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
278
278
|
if (own) {
|
|
279
279
|
return {
|
|
280
280
|
object: 'claim',
|
|
281
|
-
id: own.lease.
|
|
281
|
+
id: own.lease.id,
|
|
282
282
|
status: 'active',
|
|
283
283
|
target: own.target,
|
|
284
284
|
reason: own.reason,
|
|
@@ -403,7 +403,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
403
403
|
const effective = {
|
|
404
404
|
...opts,
|
|
405
405
|
...(autoLease ? { claim: autoLease } : {}),
|
|
406
|
-
...(isClaimHandle(claim) ? { claim: { id: claim.
|
|
406
|
+
...(isClaimHandle(claim) ? { claim: { id: claim.id } } : {}),
|
|
407
407
|
};
|
|
408
408
|
try {
|
|
409
409
|
syncClient.add(model, effective);
|
|
@@ -411,7 +411,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
411
411
|
return modelAsRow(model);
|
|
412
412
|
}
|
|
413
413
|
finally {
|
|
414
|
-
await autoLease?.release().catch(() => { });
|
|
414
|
+
await autoLease?.release?.().catch(() => { });
|
|
415
415
|
}
|
|
416
416
|
}),
|
|
417
417
|
update: guard(async (params) => {
|
|
@@ -422,7 +422,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
422
422
|
return await operations.update({ ...params, claim: handle });
|
|
423
423
|
}
|
|
424
424
|
finally {
|
|
425
|
-
await handle.release();
|
|
425
|
+
await handle.release?.();
|
|
426
426
|
}
|
|
427
427
|
}
|
|
428
428
|
const { id } = params;
|
|
@@ -439,7 +439,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
439
439
|
wait: 'confirmed',
|
|
440
440
|
readAt: claimed.snapshot.stamp,
|
|
441
441
|
onStale: 'reject',
|
|
442
|
-
claimRef: { id: claimed.lease.
|
|
442
|
+
claimRef: { id: claimed.lease.id },
|
|
443
443
|
...opts,
|
|
444
444
|
}
|
|
445
445
|
: {
|
|
@@ -454,7 +454,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
454
454
|
}
|
|
455
455
|
: {}),
|
|
456
456
|
...opts,
|
|
457
|
-
...(handle ? { claim: { id: handle.
|
|
457
|
+
...(handle ? { claim: { id: handle.id } } : {}),
|
|
458
458
|
};
|
|
459
459
|
// Local user update: `applyChanges` keeps change tracking ON so
|
|
460
460
|
// the edited fields land in `modifiedProperties` and actually get
|
|
@@ -473,7 +473,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
473
473
|
await operations.delete({ ...params, claim: handle });
|
|
474
474
|
}
|
|
475
475
|
finally {
|
|
476
|
-
await handle.release();
|
|
476
|
+
await handle.release?.();
|
|
477
477
|
}
|
|
478
478
|
return;
|
|
479
479
|
}
|
|
@@ -489,7 +489,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
489
489
|
wait: 'confirmed',
|
|
490
490
|
readAt: claimed.snapshot.stamp,
|
|
491
491
|
onStale: 'reject',
|
|
492
|
-
claimRef: { id: claimed.lease.
|
|
492
|
+
claimRef: { id: claimed.lease.id },
|
|
493
493
|
...opts,
|
|
494
494
|
}
|
|
495
495
|
: {
|
|
@@ -501,7 +501,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
501
501
|
}
|
|
502
502
|
: {}),
|
|
503
503
|
...opts,
|
|
504
|
-
...(handle ? { claim: { id: handle.
|
|
504
|
+
...(handle ? { claim: { id: handle.id } } : {}),
|
|
505
505
|
};
|
|
506
506
|
syncClient.delete(model, effective);
|
|
507
507
|
await waitForMutation(model, effective);
|
|
@@ -6,6 +6,16 @@ export interface MintSessionContext {
|
|
|
6
6
|
readonly apiKey: string;
|
|
7
7
|
readonly baseUrl: string;
|
|
8
8
|
readonly fetch?: typeof fetch;
|
|
9
|
+
/**
|
|
10
|
+
* Schema-key → wire typename map, supplied ONLY by the schema client
|
|
11
|
+
* (`Ablo`). A capability is scoped by the lowercased TYPENAME the Hub
|
|
12
|
+
* checks, but `can` is keyed by schema key — so `can: { documents: ['update'] }`
|
|
13
|
+
* on a model whose `typename` is overridden to `Document` must mint
|
|
14
|
+
* `document.update`, not `documents.update` (else the Hub denies it with
|
|
15
|
+
* `capability_scope_denied`). The schemaless `ApiClient` omits this map:
|
|
16
|
+
* there the `can` key already IS the wire token, so no translation applies.
|
|
17
|
+
*/
|
|
18
|
+
readonly modelTypenames?: Readonly<Record<string, string>>;
|
|
9
19
|
}
|
|
10
20
|
/**
|
|
11
21
|
* Mint a session token from an already-resolved `sk_` credential + base URL.
|