@abloatai/ablo 0.8.0 → 0.9.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 +40 -1
- package/README.md +32 -27
- package/dist/BaseSyncedStore.d.ts +73 -0
- package/dist/BaseSyncedStore.js +172 -2
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/agent/session.js +3 -3
- package/dist/ai-sdk/coordination-context.js +4 -0
- package/dist/ai-sdk/index.d.ts +56 -47
- package/dist/ai-sdk/index.js +56 -47
- package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
- package/dist/ai-sdk/intent-broadcast.js +11 -4
- package/dist/ai-sdk/wrap.d.ts +14 -11
- package/dist/ai-sdk/wrap.js +11 -13
- package/dist/auth/credentialSource.d.ts +34 -0
- package/dist/auth/credentialSource.js +63 -0
- package/dist/auth/index.d.ts +2 -22
- package/dist/auth/index.js +4 -42
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +160 -42
- package/dist/client/Ablo.js +145 -75
- package/dist/client/ApiClient.d.ts +20 -4
- package/dist/client/ApiClient.js +166 -28
- package/dist/client/auth.d.ts +14 -5
- package/dist/client/auth.js +60 -7
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +130 -66
- package/dist/client/createModelProxy.js +152 -49
- package/dist/client/httpClient.d.ts +71 -0
- package/dist/client/httpClient.js +69 -0
- package/dist/client/identity.d.ts +2 -6
- package/dist/client/identity.js +49 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +3 -3
- package/dist/client/registerDataSource.js +11 -9
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/core/DatabaseManager.js +30 -2
- package/dist/core/openIDBWithTimeout.d.ts +36 -0
- package/dist/core/openIDBWithTimeout.js +88 -1
- package/dist/errorCodes.d.ts +70 -1
- package/dist/errorCodes.js +108 -9
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +72 -22
- package/dist/index.d.ts +17 -8
- package/dist/index.js +15 -6
- package/dist/keys/index.d.ts +16 -1
- package/dist/keys/index.js +26 -6
- package/dist/mutators/UndoManager.d.ts +86 -50
- package/dist/mutators/UndoManager.js +129 -22
- package/dist/mutators/inverseOp.d.ts +129 -0
- package/dist/mutators/inverseOp.js +74 -0
- package/dist/mutators/readerActions.d.ts +1 -1
- package/dist/mutators/undoApply.d.ts +42 -0
- package/dist/mutators/undoApply.js +143 -0
- package/dist/query/client.d.ts +10 -9
- package/dist/query/client.js +3 -6
- package/dist/react/AbloProvider.d.ts +23 -126
- package/dist/react/AbloProvider.js +62 -199
- package/dist/react/useAblo.d.ts +2 -2
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/schema/ddl.d.ts +26 -3
- package/dist/schema/ddl.js +152 -4
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.js +12 -0
- package/dist/schema/model.d.ts +11 -0
- package/dist/schema/model.js +2 -0
- package/dist/schema/openapi.d.ts +28 -0
- package/dist/schema/openapi.js +118 -0
- package/dist/schema/plane.d.ts +23 -0
- package/dist/schema/plane.js +19 -0
- package/dist/schema/relation.d.ts +20 -0
- package/dist/schema/serialize.d.ts +4 -0
- package/dist/schema/serialize.js +4 -0
- package/dist/schema/sync-delta-row.d.ts +157 -0
- package/dist/schema/sync-delta-row.js +102 -0
- package/dist/schema/sync-delta-wire.d.ts +180 -0
- package/dist/schema/sync-delta-wire.js +102 -0
- package/dist/server/adapter.d.ts +156 -0
- package/dist/server/adapter.js +19 -0
- package/dist/server/commit.d.ts +82 -0
- package/dist/server/commit.js +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +1 -0
- package/dist/server/next.d.ts +51 -0
- package/dist/server/next.js +47 -0
- package/dist/server/read-config.d.ts +60 -0
- package/dist/server/read-config.js +8 -0
- package/dist/server/storage-mode.d.ts +17 -0
- package/dist/server/storage-mode.js +12 -0
- package/dist/source/adapter.d.ts +59 -0
- package/dist/source/adapter.js +19 -0
- package/dist/source/adapters/drizzle.d.ts +34 -0
- package/dist/source/adapters/drizzle.js +147 -0
- package/dist/source/adapters/memory.d.ts +12 -0
- package/dist/source/adapters/memory.js +114 -0
- package/dist/source/adapters/prisma.d.ts +57 -0
- package/dist/source/adapters/prisma.js +199 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +143 -0
- package/dist/source/contract.js +98 -0
- package/dist/source/index.d.ts +61 -10
- package/dist/source/index.js +98 -0
- package/dist/source/next.d.ts +33 -0
- package/dist/source/next.js +26 -0
- package/dist/sync/BootstrapHelper.d.ts +10 -0
- package/dist/sync/BootstrapHelper.js +10 -15
- package/dist/sync/ConnectionManager.d.ts +55 -1
- package/dist/sync/ConnectionManager.js +155 -16
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +238 -39
- package/dist/sync/NetworkProbe.d.ts +58 -24
- package/dist/sync/NetworkProbe.js +118 -42
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +70 -36
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/types/streams.d.ts +9 -0
- package/dist/utils/mobx-setup.js +1 -0
- package/dist/webhooks/events.d.ts +38 -0
- package/dist/webhooks/events.js +40 -0
- package/dist/webhooks/index.d.ts +10 -0
- package/dist/webhooks/index.js +10 -0
- package/dist/wire/errorEnvelope.d.ts +34 -0
- package/dist/wire/errorEnvelope.js +86 -0
- package/dist/wire/frames.d.ts +119 -0
- package/dist/wire/frames.js +1 -0
- package/dist/wire/index.d.ts +24 -0
- package/dist/wire/index.js +21 -0
- package/dist/wire/listEnvelope.d.ts +45 -0
- package/dist/wire/listEnvelope.js +17 -0
- package/docs/api.md +47 -44
- package/docs/cli.md +44 -44
- package/docs/client-behavior.md +30 -30
- package/docs/coordination.md +33 -36
- package/docs/data-sources.md +35 -15
- package/docs/examples/agent-human.md +45 -43
- package/docs/examples/ai-sdk-tool.md +20 -16
- package/docs/examples/existing-python-backend.md +16 -12
- package/docs/examples/nextjs.md +14 -12
- package/docs/examples/scoped-agent.md +1 -1
- package/docs/examples/server-agent.md +24 -21
- package/docs/guarantees.md +15 -13
- package/docs/index.md +1 -1
- package/docs/integration-guide.md +30 -30
- package/docs/interaction-model.md +19 -23
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +2 -2
- package/docs/mcp.md +6 -6
- package/docs/quickstart.md +41 -31
- package/docs/react.md +13 -9
- package/docs/schema-contract.md +12 -10
- package/docs/the-loop.md +21 -0
- package/examples/data-source/README.md +4 -5
- package/examples/data-source/customer-server.ts +27 -25
- package/llms.txt +28 -5
- package/package.json +43 -3
|
@@ -16,7 +16,7 @@ export function validateAbloOptions(input) {
|
|
|
16
16
|
const kind = options.kind ?? 'user';
|
|
17
17
|
if (!url) {
|
|
18
18
|
return new AbloValidationError('Ablo: `url` is required. Pass the sync server URL, e.g. ' +
|
|
19
|
-
`Ablo({ baseURL: 'wss://
|
|
19
|
+
`Ablo({ baseURL: 'wss://api.abloatai.com', schema, user })`, { code: 'base_url_missing' });
|
|
20
20
|
}
|
|
21
21
|
// Schema is optional for the model-first API:
|
|
22
22
|
// Ablo({ apiKey }).model('clauses').retrieve(...)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Follows Ablo's architecture for database management.
|
|
9
9
|
*/
|
|
10
10
|
import { getContext } from '../context.js';
|
|
11
|
-
import { openIDBWithTimeout } from './openIDBWithTimeout.js';
|
|
11
|
+
import { openIDBWithTimeout, deleteIDBWithTimeout, IDBOpenTimeoutError, } from './openIDBWithTimeout.js';
|
|
12
12
|
import { AbloConnectionError } from '../errors.js';
|
|
13
13
|
import { getActiveRegistry, hasActiveRegistry } from '../ModelRegistry.js';
|
|
14
14
|
/**
|
|
@@ -30,7 +30,7 @@ export class DatabaseManager {
|
|
|
30
30
|
* Initialize the meta database (ablo_databases)
|
|
31
31
|
*/
|
|
32
32
|
async initializeMetaDatabase() {
|
|
33
|
-
|
|
33
|
+
const open = () => openIDBWithTimeout(this.metaDbName, 1, {
|
|
34
34
|
onUpgrade: (request) => {
|
|
35
35
|
const db = request.result;
|
|
36
36
|
if (!db.objectStoreNames.contains('databases')) {
|
|
@@ -42,6 +42,34 @@ export class DatabaseManager {
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
});
|
|
45
|
+
try {
|
|
46
|
+
this.metaDb = await open();
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// Self-heal a wedged meta DB. When `ablo_databases`'s backing store gets
|
|
50
|
+
// stuck (a corrupted store, or a leaked connection from a prior
|
|
51
|
+
// timed-out open), every open of that name hangs with no event and the
|
|
52
|
+
// app is permanently bricked until the user manually clears site data —
|
|
53
|
+
// the "open did not resolve within 10000ms" dead end. The registry this
|
|
54
|
+
// DB holds is rebuildable from the server on the next bootstrap, so it is
|
|
55
|
+
// safe to delete and re-create. Try exactly once: delete, then re-open.
|
|
56
|
+
if (!(error instanceof IDBOpenTimeoutError))
|
|
57
|
+
throw error;
|
|
58
|
+
getContext().logger.warn('[sync-engine] meta DB open timed out — attempting self-heal (delete + retry)', { db: this.metaDbName, reason: error.reason });
|
|
59
|
+
getContext().observability.captureBootstrapFailure(error, {
|
|
60
|
+
type: 'meta-db-open-timeout',
|
|
61
|
+
});
|
|
62
|
+
const deleted = await deleteIDBWithTimeout(this.metaDbName);
|
|
63
|
+
if (!deleted) {
|
|
64
|
+
// The delete itself was blocked/stuck — a live connection in another
|
|
65
|
+
// window or a deadlocked backing store. We cannot recover in-page;
|
|
66
|
+
// rethrow so the provider surfaces the real (now actionable) error.
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
// Fresh store — this open creates `ablo_databases` from scratch.
|
|
70
|
+
this.metaDb = await open();
|
|
71
|
+
getContext().logger.info('[sync-engine] meta DB self-heal succeeded');
|
|
72
|
+
}
|
|
45
73
|
}
|
|
46
74
|
/**
|
|
47
75
|
* Calculate database info for a user/workspace combination
|
|
@@ -16,12 +16,48 @@
|
|
|
16
16
|
export declare class IDBOpenTimeoutError extends Error {
|
|
17
17
|
readonly dbName: string;
|
|
18
18
|
readonly reason: 'blocked' | 'timeout';
|
|
19
|
+
/**
|
|
20
|
+
* Stable, transport-independent code. `toAbloError` preserves a string
|
|
21
|
+
* `.code`, so this survives the wrap into `AbloError` and reaches the
|
|
22
|
+
* provider's `onError` intact — letting the app distinguish a wedged-storage
|
|
23
|
+
* failure (show a recovery screen) from any other bootstrap error without a
|
|
24
|
+
* brittle message match.
|
|
25
|
+
*/
|
|
26
|
+
readonly code = "storage_open_timeout";
|
|
19
27
|
constructor(dbName: string, reason: 'blocked' | 'timeout', message: string);
|
|
20
28
|
}
|
|
29
|
+
/** True for the wedged-IndexedDB failure, after it has been wrapped anywhere. */
|
|
30
|
+
export declare function isStorageOpenTimeout(err: unknown): boolean;
|
|
21
31
|
export interface OpenIDBOptions {
|
|
22
32
|
/** Called inside `onupgradeneeded` — mirrors `IDBOpenDBRequest.onupgradeneeded`. */
|
|
23
33
|
onUpgrade?: (request: IDBOpenDBRequest, event: IDBVersionChangeEvent) => void;
|
|
24
34
|
/** Max milliseconds to wait for the open request to resolve. Default 10_000. */
|
|
25
35
|
timeoutMs?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Called when another context (a new tab, a fresh deploy, or our own
|
|
38
|
+
* `deleteIDBWithTimeout` self-heal) fires `versionchange` on this connection.
|
|
39
|
+
* By default the connection is `close()`d immediately — the W3C/MDN-mandated
|
|
40
|
+
* behavior that lets the other context's upgrade/delete proceed instead of
|
|
41
|
+
* blocking forever. Provide this to ALSO react (e.g. prompt a reload) AFTER
|
|
42
|
+
* the close. Throwing here is swallowed.
|
|
43
|
+
*/
|
|
44
|
+
onVersionChange?: () => void;
|
|
26
45
|
}
|
|
27
46
|
export declare function openIDBWithTimeout(name: string, version: number | undefined, options?: OpenIDBOptions): Promise<IDBDatabase>;
|
|
47
|
+
/**
|
|
48
|
+
* Bounded `indexedDB.deleteDatabase()` — the delete counterpart of
|
|
49
|
+
* `openIDBWithTimeout`. Used by the meta-DB self-heal: when opening
|
|
50
|
+
* `ablo_databases` times out (a wedged backing store), we attempt to delete it
|
|
51
|
+
* and re-create from scratch. The registry it holds is rebuildable from the
|
|
52
|
+
* server on the next bootstrap, so dropping it is safe.
|
|
53
|
+
*
|
|
54
|
+
* Like `open`, `deleteDatabase` can hang indefinitely: if another live
|
|
55
|
+
* connection holds the DB it fires `onblocked` and waits, and on a truly stuck
|
|
56
|
+
* store it fires *no* event at all. Both become a bounded rejection here so the
|
|
57
|
+
* caller can fall through to surfacing a real error instead of spinning.
|
|
58
|
+
*
|
|
59
|
+
* Resolves `true` on a clean delete, `false` if it was blocked or timed out
|
|
60
|
+
* (caller decides whether to retry the open regardless — a no-op delete still
|
|
61
|
+
* leaves us no worse off).
|
|
62
|
+
*/
|
|
63
|
+
export declare function deleteIDBWithTimeout(name: string, timeoutMs?: number): Promise<boolean>;
|
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
export class IDBOpenTimeoutError extends Error {
|
|
17
17
|
dbName;
|
|
18
18
|
reason;
|
|
19
|
+
/**
|
|
20
|
+
* Stable, transport-independent code. `toAbloError` preserves a string
|
|
21
|
+
* `.code`, so this survives the wrap into `AbloError` and reaches the
|
|
22
|
+
* provider's `onError` intact — letting the app distinguish a wedged-storage
|
|
23
|
+
* failure (show a recovery screen) from any other bootstrap error without a
|
|
24
|
+
* brittle message match.
|
|
25
|
+
*/
|
|
26
|
+
code = 'storage_open_timeout';
|
|
19
27
|
constructor(dbName, reason, message) {
|
|
20
28
|
super(message);
|
|
21
29
|
this.dbName = dbName;
|
|
@@ -23,6 +31,12 @@ export class IDBOpenTimeoutError extends Error {
|
|
|
23
31
|
this.name = 'IDBOpenTimeoutError';
|
|
24
32
|
}
|
|
25
33
|
}
|
|
34
|
+
/** True for the wedged-IndexedDB failure, after it has been wrapped anywhere. */
|
|
35
|
+
export function isStorageOpenTimeout(err) {
|
|
36
|
+
return (typeof err === 'object' &&
|
|
37
|
+
err !== null &&
|
|
38
|
+
err.code === 'storage_open_timeout');
|
|
39
|
+
}
|
|
26
40
|
export function openIDBWithTimeout(name, version, options = {}) {
|
|
27
41
|
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
28
42
|
return new Promise((resolve, reject) => {
|
|
@@ -42,7 +56,47 @@ export function openIDBWithTimeout(name, version, options = {}) {
|
|
|
42
56
|
options.onUpgrade(request, event);
|
|
43
57
|
};
|
|
44
58
|
}
|
|
45
|
-
request.onsuccess = () =>
|
|
59
|
+
request.onsuccess = () => {
|
|
60
|
+
// If we ALREADY timed out (or blocked) and rejected, this is a late
|
|
61
|
+
// success: the native open eventually completed after we gave up. The
|
|
62
|
+
// resulting connection is orphaned — nobody up the stack holds it, so
|
|
63
|
+
// nobody will `.close()` it. A leaked open connection holds an IndexedDB
|
|
64
|
+
// lock that wedges every subsequent open/delete of this DB name (the
|
|
65
|
+
// exact "ablo_databases open/delete hangs forever with no event" failure
|
|
66
|
+
// mode). Close it here so a timed-out attempt can't poison the store.
|
|
67
|
+
if (settled) {
|
|
68
|
+
try {
|
|
69
|
+
request.result.close();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Best-effort — a half-open connection may already be unusable.
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const db = request.result;
|
|
77
|
+
// MANDATORY resilience handler (W3C IndexedDB / MDN): close this
|
|
78
|
+
// connection the instant any other context wants to upgrade or delete the
|
|
79
|
+
// DB. Without it, an open connection that ignores `versionchange` blocks
|
|
80
|
+
// the other context's request indefinitely — the root cause of a wedged
|
|
81
|
+
// `ablo_databases` that survives reloads (an interrupted transaction's
|
|
82
|
+
// connection never closes, so every later open/delete hangs with no
|
|
83
|
+
// event). Auto-closing here makes the store self-releasing.
|
|
84
|
+
db.onversionchange = () => {
|
|
85
|
+
try {
|
|
86
|
+
db.close();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Already closing/closed — nothing to do.
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
options.onVersionChange?.();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// A consumer reaction must never break the close.
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
settle(() => resolve(db));
|
|
99
|
+
};
|
|
46
100
|
request.onerror = () => settle(() => reject(request.error));
|
|
47
101
|
// The critical handler: another tab is blocking us. Native API leaves
|
|
48
102
|
// the request pending indefinitely; we fail fast with a clear error so
|
|
@@ -61,3 +115,36 @@ export function openIDBWithTimeout(name, version, options = {}) {
|
|
|
61
115
|
}, timeoutMs);
|
|
62
116
|
});
|
|
63
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Bounded `indexedDB.deleteDatabase()` — the delete counterpart of
|
|
120
|
+
* `openIDBWithTimeout`. Used by the meta-DB self-heal: when opening
|
|
121
|
+
* `ablo_databases` times out (a wedged backing store), we attempt to delete it
|
|
122
|
+
* and re-create from scratch. The registry it holds is rebuildable from the
|
|
123
|
+
* server on the next bootstrap, so dropping it is safe.
|
|
124
|
+
*
|
|
125
|
+
* Like `open`, `deleteDatabase` can hang indefinitely: if another live
|
|
126
|
+
* connection holds the DB it fires `onblocked` and waits, and on a truly stuck
|
|
127
|
+
* store it fires *no* event at all. Both become a bounded rejection here so the
|
|
128
|
+
* caller can fall through to surfacing a real error instead of spinning.
|
|
129
|
+
*
|
|
130
|
+
* Resolves `true` on a clean delete, `false` if it was blocked or timed out
|
|
131
|
+
* (caller decides whether to retry the open regardless — a no-op delete still
|
|
132
|
+
* leaves us no worse off).
|
|
133
|
+
*/
|
|
134
|
+
export function deleteIDBWithTimeout(name, timeoutMs = 5_000) {
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
let settled = false;
|
|
137
|
+
const settle = (value) => {
|
|
138
|
+
if (settled)
|
|
139
|
+
return;
|
|
140
|
+
settled = true;
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
resolve(value);
|
|
143
|
+
};
|
|
144
|
+
const request = indexedDB.deleteDatabase(name);
|
|
145
|
+
request.onsuccess = () => settle(true);
|
|
146
|
+
request.onerror = () => settle(false);
|
|
147
|
+
request.onblocked = () => settle(false);
|
|
148
|
+
const timer = setTimeout(() => settle(false), timeoutMs);
|
|
149
|
+
});
|
|
150
|
+
}
|
package/dist/errorCodes.d.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* carry no `httpStatus`, exactly as Stripe omits client-side
|
|
30
30
|
* programmer errors from its published code list.
|
|
31
31
|
*/
|
|
32
|
+
import { z } from 'zod';
|
|
32
33
|
/**
|
|
33
34
|
* Version of the error contract — the envelope shape + the set of codes and
|
|
34
35
|
* their semantics. Date-based, like Stripe's API versions. Bump it (and only
|
|
@@ -36,9 +37,48 @@
|
|
|
36
37
|
* code, a changed HTTP status, an envelope field. Emitted in `errors.json`
|
|
37
38
|
* and on the `Ablo-Version` response header so a consumer can detect drift.
|
|
38
39
|
*/
|
|
39
|
-
export declare const ERROR_CONTRACT_VERSION = "2026-
|
|
40
|
+
export declare const ERROR_CONTRACT_VERSION = "2026-06-02";
|
|
40
41
|
/** Coarse grouping for metrics dashboards and docs sectioning. */
|
|
41
42
|
export type ErrorCategory = 'auth' | 'permission' | 'capability' | 'claim' | 'conflict' | 'validation' | 'not_found' | 'tenant' | 'schema' | 'intent' | 'bootstrap' | 'transport' | 'rate_limit' | 'server' | 'client';
|
|
43
|
+
/**
|
|
44
|
+
* The closed taxonomy of *how a failure recovers* — one rung above the raw
|
|
45
|
+
* `code`. Where `code` says **what** went wrong, `RecoveryClass` says **what
|
|
46
|
+
* the client should do about it**, which is exactly the discriminant the sync
|
|
47
|
+
* FSM and the network probe need. It collapses what used to be three scattered
|
|
48
|
+
* booleans (`retryable`, `authBlocked`, `sessionValid`) into one exhaustive,
|
|
49
|
+
* Zod-validated enum so the connection layer branches on a single value with
|
|
50
|
+
* compile-time completeness instead of ad-hoc `if (!isRetryableCode(...))`
|
|
51
|
+
* chains.
|
|
52
|
+
*
|
|
53
|
+
* - `access_credential_expiry` — the Stripe-style ephemeral key (`ek_`/`rk_`)
|
|
54
|
+
* the sync-engine presents as its Bearer has expired. The long-lived login
|
|
55
|
+
* is fine; the remedy is to silently RE-MINT a fresh key from the session
|
|
56
|
+
* and retry the same request. This MUST NOT sign the user out (the whole
|
|
57
|
+
* point of the wake-from-sleep fix: a 15-min `ek_` dying after a laptop nap
|
|
58
|
+
* is routine, not a logout).
|
|
59
|
+
* - `session_expiry` — the LONG-LIVED login itself is gone. Terminal:
|
|
60
|
+
* sign out and route to re-authentication.
|
|
61
|
+
* - `auth_blocked` — reachable, but the credential TYPE/config was rejected
|
|
62
|
+
* (wrong key kind, untrusted issuer, no org). Re-auth re-mints the same
|
|
63
|
+
* rejected credential and loops, so STOP — don't reconnect, don't sign out.
|
|
64
|
+
* - `permission` — a 403 authorization denial (scope/role/membership).
|
|
65
|
+
* - `transient` — retry the same request unchanged (5xx, lease contention…).
|
|
66
|
+
* - `none` — not a recoverable-auth condition (validation, not-found, local
|
|
67
|
+
* invariants, and any forward-compat code an older SDK doesn't know).
|
|
68
|
+
*/
|
|
69
|
+
export declare const RECOVERY_CLASSES: readonly ["access_credential_expiry", "session_expiry", "auth_blocked", "permission", "transient", "none"];
|
|
70
|
+
/** Zod enum derived from {@link RECOVERY_CLASSES} — the runtime-validatable
|
|
71
|
+
* form of the recovery taxonomy. */
|
|
72
|
+
export declare const recoveryClassSchema: z.ZodEnum<{
|
|
73
|
+
permission: "permission";
|
|
74
|
+
access_credential_expiry: "access_credential_expiry";
|
|
75
|
+
session_expiry: "session_expiry";
|
|
76
|
+
auth_blocked: "auth_blocked";
|
|
77
|
+
transient: "transient";
|
|
78
|
+
none: "none";
|
|
79
|
+
}>;
|
|
80
|
+
/** How a failure recovers. See {@link RECOVERY_CLASSES}. */
|
|
81
|
+
export type RecoveryClass = z.infer<typeof recoveryClassSchema>;
|
|
42
82
|
/** One registry entry. `httpStatus` is present only for `surface: 'wire'`
|
|
43
83
|
* codes — status is a property of the wire boundary, never of a
|
|
44
84
|
* purely-local client invariant. */
|
|
@@ -55,6 +95,14 @@ export interface ErrorCodeSpec {
|
|
|
55
95
|
readonly retryable: boolean;
|
|
56
96
|
/** One-line human description — the source text for the `doc_url` page. */
|
|
57
97
|
readonly message: string;
|
|
98
|
+
/**
|
|
99
|
+
* Explicit recovery class. Set ONLY where it diverges from what `category` /
|
|
100
|
+
* `httpStatus` / `retryable` already imply — i.e. the handful of auth codes
|
|
101
|
+
* whose remedy (`session_expiry` vs `access_credential_expiry`) the bare
|
|
102
|
+
* status can't distinguish. Everything else is derived by
|
|
103
|
+
* {@link classifyRecovery}, so adding a normal code needs no `recovery`.
|
|
104
|
+
*/
|
|
105
|
+
readonly recovery?: RecoveryClass;
|
|
58
106
|
}
|
|
59
107
|
/**
|
|
60
108
|
* The closed set of stable error codes. Add a code here BEFORE throwing it
|
|
@@ -92,6 +140,7 @@ export declare const ERROR_CODES: {
|
|
|
92
140
|
readonly byo_role_cannot_enforce_rls: ErrorCodeSpec;
|
|
93
141
|
readonly byo_role_unreadable: ErrorCodeSpec;
|
|
94
142
|
readonly byo_tenant_tables_unforced_rls: ErrorCodeSpec;
|
|
143
|
+
readonly byo_host_not_allowed: ErrorCodeSpec;
|
|
95
144
|
readonly claim_conflict: ErrorCodeSpec;
|
|
96
145
|
readonly claim_lost: ErrorCodeSpec;
|
|
97
146
|
readonly entity_claimed: ErrorCodeSpec;
|
|
@@ -167,11 +216,13 @@ export declare const ERROR_CODES: {
|
|
|
167
216
|
readonly queue_too_deep: ErrorCodeSpec;
|
|
168
217
|
readonly flush_timeout: ErrorCodeSpec;
|
|
169
218
|
readonly wait_for_timeout: ErrorCodeSpec;
|
|
219
|
+
readonly instance_at_capacity: ErrorCodeSpec;
|
|
170
220
|
readonly fetch_unavailable: ErrorCodeSpec;
|
|
171
221
|
readonly base_url_missing: ErrorCodeSpec;
|
|
172
222
|
readonly sync_not_ready: ErrorCodeSpec;
|
|
173
223
|
readonly ws_not_ready: ErrorCodeSpec;
|
|
174
224
|
readonly quota_exceeded: ErrorCodeSpec;
|
|
225
|
+
readonly connection_limit_exceeded: ErrorCodeSpec;
|
|
175
226
|
readonly internal_error: ErrorCodeSpec;
|
|
176
227
|
readonly quota_lookup_failed: ErrorCodeSpec;
|
|
177
228
|
readonly turn_open_failed: ErrorCodeSpec;
|
|
@@ -211,6 +262,7 @@ export declare const ERROR_CODES: {
|
|
|
211
262
|
readonly mutator_registry_unnamed_def: ErrorCodeSpec;
|
|
212
263
|
readonly mutators_schema_missing: ErrorCodeSpec;
|
|
213
264
|
readonly undo_scope_schema_missing: ErrorCodeSpec;
|
|
265
|
+
readonly undo_entry_invalid: ErrorCodeSpec;
|
|
214
266
|
readonly mock_mutation_failed: ErrorCodeSpec;
|
|
215
267
|
readonly mock_unsupported_operation: ErrorCodeSpec;
|
|
216
268
|
readonly invalid_body: ErrorCodeSpec;
|
|
@@ -284,3 +336,20 @@ export declare function errorCodeSpec(code: string): ErrorCodeSpec | undefined;
|
|
|
284
336
|
/** Whether a code's spec marks it retryable. Unknown / dynamic codes
|
|
285
337
|
* default to non-retryable (safe default — don't auto-retry the unknown). */
|
|
286
338
|
export declare function isRetryableCode(code: string): boolean;
|
|
339
|
+
/**
|
|
340
|
+
* Classify a `code` into its {@link RecoveryClass} — the single discriminant
|
|
341
|
+
* the connection FSM and the network probe branch on.
|
|
342
|
+
*
|
|
343
|
+
* The registry stays the source of truth: an explicit `spec.recovery` wins
|
|
344
|
+
* (set only on the few auth codes whose remedy the status can't reveal), and
|
|
345
|
+
* everything else is DERIVED from the spec so the registry stays terse:
|
|
346
|
+
* - retryable → `transient`
|
|
347
|
+
* - 403 → `permission`
|
|
348
|
+
* - residual `auth`-category → `auth_blocked` (the 401 credential-type codes)
|
|
349
|
+
* - otherwise / unknown → `none`
|
|
350
|
+
*
|
|
351
|
+
* Unknown / dynamic `policy:*` / forward-compat codes (`spec === undefined`)
|
|
352
|
+
* default to `none`, mirroring {@link isRetryableCode}'s safe default — never
|
|
353
|
+
* silently treat an unrecognised code as a credential expiry or a logout.
|
|
354
|
+
*/
|
|
355
|
+
export declare function classifyRecovery(code: string): RecoveryClass;
|
package/dist/errorCodes.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* carry no `httpStatus`, exactly as Stripe omits client-side
|
|
30
30
|
* programmer errors from its published code list.
|
|
31
31
|
*/
|
|
32
|
+
import { z } from 'zod';
|
|
32
33
|
/**
|
|
33
34
|
* Version of the error contract — the envelope shape + the set of codes and
|
|
34
35
|
* their semantics. Date-based, like Stripe's API versions. Bump it (and only
|
|
@@ -36,8 +37,45 @@
|
|
|
36
37
|
* code, a changed HTTP status, an envelope field. Emitted in `errors.json`
|
|
37
38
|
* and on the `Ablo-Version` response header so a consumer can detect drift.
|
|
38
39
|
*/
|
|
39
|
-
export const ERROR_CONTRACT_VERSION = '2026-
|
|
40
|
-
|
|
40
|
+
export const ERROR_CONTRACT_VERSION = '2026-06-02';
|
|
41
|
+
/**
|
|
42
|
+
* The closed taxonomy of *how a failure recovers* — one rung above the raw
|
|
43
|
+
* `code`. Where `code` says **what** went wrong, `RecoveryClass` says **what
|
|
44
|
+
* the client should do about it**, which is exactly the discriminant the sync
|
|
45
|
+
* FSM and the network probe need. It collapses what used to be three scattered
|
|
46
|
+
* booleans (`retryable`, `authBlocked`, `sessionValid`) into one exhaustive,
|
|
47
|
+
* Zod-validated enum so the connection layer branches on a single value with
|
|
48
|
+
* compile-time completeness instead of ad-hoc `if (!isRetryableCode(...))`
|
|
49
|
+
* chains.
|
|
50
|
+
*
|
|
51
|
+
* - `access_credential_expiry` — the Stripe-style ephemeral key (`ek_`/`rk_`)
|
|
52
|
+
* the sync-engine presents as its Bearer has expired. The long-lived login
|
|
53
|
+
* is fine; the remedy is to silently RE-MINT a fresh key from the session
|
|
54
|
+
* and retry the same request. This MUST NOT sign the user out (the whole
|
|
55
|
+
* point of the wake-from-sleep fix: a 15-min `ek_` dying after a laptop nap
|
|
56
|
+
* is routine, not a logout).
|
|
57
|
+
* - `session_expiry` — the LONG-LIVED login itself is gone. Terminal:
|
|
58
|
+
* sign out and route to re-authentication.
|
|
59
|
+
* - `auth_blocked` — reachable, but the credential TYPE/config was rejected
|
|
60
|
+
* (wrong key kind, untrusted issuer, no org). Re-auth re-mints the same
|
|
61
|
+
* rejected credential and loops, so STOP — don't reconnect, don't sign out.
|
|
62
|
+
* - `permission` — a 403 authorization denial (scope/role/membership).
|
|
63
|
+
* - `transient` — retry the same request unchanged (5xx, lease contention…).
|
|
64
|
+
* - `none` — not a recoverable-auth condition (validation, not-found, local
|
|
65
|
+
* invariants, and any forward-compat code an older SDK doesn't know).
|
|
66
|
+
*/
|
|
67
|
+
export const RECOVERY_CLASSES = [
|
|
68
|
+
'access_credential_expiry',
|
|
69
|
+
'session_expiry',
|
|
70
|
+
'auth_blocked',
|
|
71
|
+
'permission',
|
|
72
|
+
'transient',
|
|
73
|
+
'none',
|
|
74
|
+
];
|
|
75
|
+
/** Zod enum derived from {@link RECOVERY_CLASSES} — the runtime-validatable
|
|
76
|
+
* form of the recovery taxonomy. */
|
|
77
|
+
export const recoveryClassSchema = z.enum(RECOVERY_CLASSES);
|
|
78
|
+
const wire = (category, httpStatus, retryable, message, recovery) => ({ category, surface: 'wire', httpStatus, retryable, message, recovery });
|
|
41
79
|
const client = (category, message) => ({ category, surface: 'client', retryable: false, message });
|
|
42
80
|
/**
|
|
43
81
|
* The closed set of stable error codes. Add a code here BEFORE throwing it
|
|
@@ -47,7 +85,13 @@ export const ERROR_CODES = {
|
|
|
47
85
|
// ── auth (401) ─────────────────────────────────────────────────────
|
|
48
86
|
apikey_invalid: wire('auth', 401, false, 'API key is unknown or malformed.'),
|
|
49
87
|
apikey_revoked: wire('auth', 401, false, 'API key has been revoked.'),
|
|
50
|
-
|
|
88
|
+
// THE sync-engine access credential — the Stripe-style ephemeral key
|
|
89
|
+
// (`ek_` for users, `rk_` for agents) minted server-side from the login and
|
|
90
|
+
// presented as a Bearer. Its expiry is routine and re-mintable: get a fresh
|
|
91
|
+
// key from the still-valid session and retry — NEVER a sign-out. (An agent's
|
|
92
|
+
// expired `rk_` must not log a human out either.) This is the ONLY code on
|
|
93
|
+
// the silent re-mint path; see RecoveryClass `access_credential_expiry`.
|
|
94
|
+
apikey_expired: wire('auth', 401, false, 'API key has expired.', 'access_credential_expiry'),
|
|
51
95
|
apikey_missing: wire('auth', 401, false, 'No API key was supplied on the request.'),
|
|
52
96
|
api_key_required: wire('auth', 401, false, 'This operation requires an API key.'),
|
|
53
97
|
capability_id_missing: wire('auth', 401, false, 'A capability id was expected but not provided.'),
|
|
@@ -55,7 +99,8 @@ export const ERROR_CODES = {
|
|
|
55
99
|
identity_resolve_failed: wire('auth', 401, false, 'Identity resolution was rejected.'),
|
|
56
100
|
auth_no_credentials: wire('auth', 401, false, 'No recognized authentication credential was presented — no API key and no bearer JWT. Send `Authorization: Bearer <token>`.'),
|
|
57
101
|
identity_missing_organization: wire('auth', 401, false, 'Authentication succeeded but resolved to no organization context.'),
|
|
58
|
-
|
|
102
|
+
// The long-lived login is gone — terminal, drives sign-out + re-auth.
|
|
103
|
+
session_expired: wire('auth', 401, false, 'The session is invalid or expired; re-authenticate.', 'session_expiry'),
|
|
59
104
|
// `jwt_invalid` is the residual fallback; the codes below split out the
|
|
60
105
|
// specific failure modes so an integrating customer can tell "I registered
|
|
61
106
|
// the wrong JWKS" from "my token has no org claim" from "wrong audience"
|
|
@@ -68,19 +113,25 @@ export const ERROR_CODES = {
|
|
|
68
113
|
jwt_audience_mismatch: wire('auth', 401, false, "The bearer JWT's `aud` (audience) claim does not match the audience this issuer is registered with."),
|
|
69
114
|
jwt_missing_subject: wire('auth', 401, false, 'The bearer JWT has no `sub` (subject) claim to identify the user.'),
|
|
70
115
|
jwt_missing_organization: wire('auth', 401, false, 'The bearer JWT carries no organization context — neither a fixed org for the issuer nor the configured organization claim.'),
|
|
71
|
-
|
|
116
|
+
// Trusted-issuer / BYO-IdP path only — Ablo's own sync-engine no longer
|
|
117
|
+
// authenticates with JWTs (it uses the Stripe-style ephemeral key, below).
|
|
118
|
+
// When a customer DOES present an external-IdP JWT, its expiry means
|
|
119
|
+
// re-authenticate against that IdP, so it classifies as a session expiry
|
|
120
|
+
// (which also keeps `isSessionErrorResponse` behaviour unchanged).
|
|
121
|
+
jwt_expired: wire('auth', 401, false, 'The bearer JWT has expired; obtain a fresh token.', 'session_expiry'),
|
|
72
122
|
jwt_org_membership_denied: wire('auth', 403, false, "The bearer JWT's subject is not an active member of the organization in its `org_id` claim (removed, suspended, or the claim does not match a membership)."),
|
|
73
123
|
file_upload_auth_required: wire('auth', 401, false, 'File upload requires an authenticated session.'),
|
|
74
124
|
browser_apikey_blocked: client('auth', 'Raw API keys must not be used from a browser context.'),
|
|
75
125
|
browser_database_url_blocked: client('auth', 'A database connection string must not be used from a browser context — it carries DB credentials.'),
|
|
76
|
-
datasource_registration_failed: client('auth', 'Failed to register the provided databaseUrl
|
|
126
|
+
datasource_registration_failed: client('auth', 'Failed to register the provided databaseUrl for the direct Postgres connector.'),
|
|
77
127
|
// ── permission / capability (403) ──────────────────────────────────
|
|
78
128
|
capability_scope_denied: wire('capability', 403, false, "The connection's resolved scope does not cover the attempted action."),
|
|
79
129
|
issuer_register_forbidden: wire('permission', 403, false, 'Registering a trusted issuer requires a secret (sk_) API key.'),
|
|
80
130
|
capability_invalid: wire('capability', 403, false, 'The capability is unknown, revoked, or expired.'),
|
|
81
|
-
byo_role_cannot_enforce_rls: wire('permission', 403, false, 'The
|
|
82
|
-
byo_role_unreadable: wire('permission', 403, false, 'The
|
|
83
|
-
byo_tenant_tables_unforced_rls: wire('permission', 403, false, 'Tenant tables do not have RLS forced under the
|
|
131
|
+
byo_role_cannot_enforce_rls: wire('permission', 403, false, 'The direct Postgres connector role cannot enforce row-level security.'),
|
|
132
|
+
byo_role_unreadable: wire('permission', 403, false, 'The direct Postgres connector role could not be introspected.'),
|
|
133
|
+
byo_tenant_tables_unforced_rls: wire('permission', 403, false, 'Tenant tables do not have RLS forced under the direct Postgres connector role.'),
|
|
134
|
+
byo_host_not_allowed: wire('permission', 403, false, 'The direct Postgres connector host resolves to a private, loopback, or link-local address and cannot be used.'),
|
|
84
135
|
// ── claim / intent conflict (409) ──────────────────────────────────
|
|
85
136
|
claim_conflict: wire('claim', 409, true, 'The target entity is claimed by another participant.'),
|
|
86
137
|
claim_lost: wire('claim', 409, true, 'A previously held claim was lost before the write applied.'),
|
|
@@ -172,12 +223,14 @@ export const ERROR_CODES = {
|
|
|
172
223
|
queue_too_deep: wire('transport', 503, true, 'The transaction queue exceeded its depth limit.'),
|
|
173
224
|
flush_timeout: wire('transport', 504, true, 'Timed out flushing the transaction queue.'),
|
|
174
225
|
wait_for_timeout: wire('transport', 504, true, 'A wait-for condition timed out.'),
|
|
226
|
+
instance_at_capacity: wire('transport', 503, true, 'The server is at connection capacity. Retry shortly — transient and not specific to your credentials.'),
|
|
175
227
|
fetch_unavailable: client('transport', 'No fetch implementation is available in this environment.'),
|
|
176
228
|
base_url_missing: client('transport', 'No base URL was configured for the client.'),
|
|
177
229
|
sync_not_ready: client('transport', 'A sync operation was attempted before the client was ready.'),
|
|
178
230
|
ws_not_ready: client('transport', 'A frame was sent before the WebSocket was connected.'),
|
|
179
231
|
// ── quota / rate limit (429) ──────────────────────────────────────
|
|
180
232
|
quota_exceeded: wire('rate_limit', 429, true, 'The organization exceeded its configured usage quota.'),
|
|
233
|
+
connection_limit_exceeded: wire('rate_limit', 429, true, 'Too many concurrent WebSocket connections for this principal or organization. Close idle connections, or retry once others drain.'),
|
|
181
234
|
// ── server (5xx) ───────────────────────────────────────────────────
|
|
182
235
|
internal_error: wire('server', 500, true, 'An unexpected server error occurred.'),
|
|
183
236
|
quota_lookup_failed: wire('server', 503, true, 'The quota decision could not be loaded.'),
|
|
@@ -219,6 +272,7 @@ export const ERROR_CODES = {
|
|
|
219
272
|
mutator_registry_unnamed_def: client('client', 'A mutator definition was registered without a name.'),
|
|
220
273
|
mutators_schema_missing: client('client', 'Mutators were registered without a schema.'),
|
|
221
274
|
undo_scope_schema_missing: client('client', 'An undo scope was opened without a schema.'),
|
|
275
|
+
undo_entry_invalid: client('client', 'An undo entry failed inverse-op schema validation.'),
|
|
222
276
|
mock_mutation_failed: client('client', 'A mock mutation adapter was configured to fail.'),
|
|
223
277
|
mock_unsupported_operation: client('client', 'A mock adapter received an unsupported operation.'),
|
|
224
278
|
// ── HTTP route edge codes (egress through app.onError) ─────────────
|
|
@@ -282,3 +336,48 @@ export function errorCodeSpec(code) {
|
|
|
282
336
|
export function isRetryableCode(code) {
|
|
283
337
|
return errorCodeSpec(code)?.retryable ?? false;
|
|
284
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Classify a `code` into its {@link RecoveryClass} — the single discriminant
|
|
341
|
+
* the connection FSM and the network probe branch on.
|
|
342
|
+
*
|
|
343
|
+
* The registry stays the source of truth: an explicit `spec.recovery` wins
|
|
344
|
+
* (set only on the few auth codes whose remedy the status can't reveal), and
|
|
345
|
+
* everything else is DERIVED from the spec so the registry stays terse:
|
|
346
|
+
* - retryable → `transient`
|
|
347
|
+
* - 403 → `permission`
|
|
348
|
+
* - residual `auth`-category → `auth_blocked` (the 401 credential-type codes)
|
|
349
|
+
* - otherwise / unknown → `none`
|
|
350
|
+
*
|
|
351
|
+
* Unknown / dynamic `policy:*` / forward-compat codes (`spec === undefined`)
|
|
352
|
+
* default to `none`, mirroring {@link isRetryableCode}'s safe default — never
|
|
353
|
+
* silently treat an unrecognised code as a credential expiry or a logout.
|
|
354
|
+
*/
|
|
355
|
+
export function classifyRecovery(code) {
|
|
356
|
+
const spec = errorCodeSpec(code);
|
|
357
|
+
if (!spec)
|
|
358
|
+
return 'none';
|
|
359
|
+
if (spec.recovery)
|
|
360
|
+
return spec.recovery;
|
|
361
|
+
if (spec.retryable)
|
|
362
|
+
return 'transient';
|
|
363
|
+
if (spec.httpStatus === 403)
|
|
364
|
+
return 'permission';
|
|
365
|
+
if (spec.category === 'auth')
|
|
366
|
+
return 'auth_blocked';
|
|
367
|
+
return 'none';
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Compile-time exhaustiveness guard: forces every {@link RecoveryClass} to be
|
|
371
|
+
* acknowledged here, so adding a class to {@link RECOVERY_CLASSES} without
|
|
372
|
+
* deciding its meaning is a type error rather than a silent gap. (Mirrors the
|
|
373
|
+
* closed-union discipline `ERROR_CODES` itself uses via `satisfies`.)
|
|
374
|
+
*/
|
|
375
|
+
const _RECOVERY_CLASS_EXHAUSTIVE = {
|
|
376
|
+
access_credential_expiry: true,
|
|
377
|
+
session_expiry: true,
|
|
378
|
+
auth_blocked: true,
|
|
379
|
+
permission: true,
|
|
380
|
+
transient: true,
|
|
381
|
+
none: true,
|
|
382
|
+
};
|
|
383
|
+
void _RECOVERY_CLASS_EXHAUSTIVE;
|
package/dist/errors.d.ts
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* Both work on every subclass.
|
|
20
20
|
*/
|
|
21
21
|
import type { ErrorCode } from './errorCodes.js';
|
|
22
|
-
export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec } from './errorCodes.js';
|
|
23
|
-
export { ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode } from './errorCodes.js';
|
|
22
|
+
export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec, RecoveryClass } from './errorCodes.js';
|
|
23
|
+
export { ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, classifyRecovery, recoveryClassSchema, RECOVERY_CLASSES, } from './errorCodes.js';
|
|
24
24
|
/** Common shape for all errors thrown by this SDK. */
|
|
25
25
|
export declare class AbloError extends Error {
|
|
26
26
|
/** Discriminator string — matches the class name. Lets consumers
|