@abloatai/ablo 0.6.0 → 0.8.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 +77 -0
- package/README.md +95 -57
- package/dist/BaseSyncedStore.d.ts +1 -1
- package/dist/BaseSyncedStore.js +8 -4
- package/dist/SyncEngineContext.d.ts +2 -1
- package/dist/SyncEngineContext.js +5 -3
- package/dist/agent/session.js +3 -2
- package/dist/auth/index.js +39 -11
- package/dist/client/Ablo.d.ts +112 -3
- package/dist/client/Ablo.js +144 -10
- package/dist/client/ApiClient.d.ts +32 -0
- package/dist/client/ApiClient.js +76 -44
- package/dist/client/auth.d.ts +11 -1
- package/dist/client/auth.js +21 -2
- package/dist/client/createModelProxy.d.ts +120 -53
- package/dist/client/createModelProxy.js +66 -31
- package/dist/client/identity.js +14 -0
- package/dist/client/registerDataSource.d.ts +19 -0
- package/dist/client/registerDataSource.js +57 -0
- package/dist/client/validateAbloOptions.d.ts +2 -1
- package/dist/client/validateAbloOptions.js +8 -7
- package/dist/coordination/index.d.ts +6 -0
- package/dist/coordination/index.js +6 -0
- package/dist/coordination/schema.d.ts +329 -0
- package/dist/coordination/schema.js +209 -0
- package/dist/core/QueryView.d.ts +4 -1
- package/dist/core/QueryView.js +1 -1
- package/dist/core/query-utils.d.ts +7 -10
- package/dist/core/query-utils.js +2 -3
- package/dist/errorCodes.d.ts +286 -0
- package/dist/errorCodes.js +284 -0
- package/dist/errors.d.ts +103 -7
- package/dist/errors.js +192 -41
- package/dist/index.d.ts +11 -6
- package/dist/index.js +10 -6
- package/dist/keys/index.d.ts +61 -0
- package/dist/keys/index.js +151 -0
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/index.js +1 -1
- package/dist/policy/types.d.ts +31 -0
- package/dist/policy/types.js +15 -0
- package/dist/query/client.js +19 -8
- package/dist/react/AbloProvider.d.ts +37 -0
- package/dist/react/AbloProvider.js +107 -4
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/SyncGroupProvider.d.ts +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +3 -2
- package/dist/react/useAblo.d.ts +4 -4
- package/dist/react/useAblo.js +10 -5
- package/dist/react/useReactive.js +16 -3
- package/dist/schema/ddl.d.ts +62 -0
- package/dist/schema/ddl.js +317 -0
- package/dist/schema/diff.d.ts +6 -0
- package/dist/schema/diff.js +21 -3
- package/dist/schema/field.d.ts +16 -19
- package/dist/schema/field.js +30 -17
- package/dist/schema/index.d.ts +7 -4
- package/dist/schema/index.js +9 -3
- package/dist/schema/model.d.ts +87 -25
- package/dist/schema/model.js +33 -3
- package/dist/schema/relation.d.ts +17 -0
- package/dist/schema/roles.d.ts +148 -0
- package/dist/schema/roles.js +149 -0
- package/dist/schema/schema.d.ts +2 -112
- package/dist/schema/schema.js +50 -62
- package/dist/schema/select.d.ts +25 -0
- package/dist/schema/select.js +55 -0
- package/dist/schema/serialize.d.ts +16 -12
- package/dist/schema/serialize.js +16 -12
- package/dist/schema/sugar.d.ts +20 -3
- package/dist/schema/sugar.js +5 -1
- package/dist/schema/tenancy.d.ts +66 -0
- package/dist/schema/tenancy.js +58 -0
- package/dist/sync/BootstrapHelper.js +46 -27
- package/dist/sync/ConnectionManager.d.ts +3 -1
- package/dist/sync/ConnectionManager.js +37 -1
- package/dist/sync/HydrationCoordinator.d.ts +2 -0
- package/dist/sync/HydrationCoordinator.js +26 -19
- package/dist/sync/NetworkProbe.d.ts +8 -0
- package/dist/sync/NetworkProbe.js +24 -2
- package/dist/sync/SyncWebSocket.d.ts +1 -1
- package/dist/sync/SyncWebSocket.js +43 -53
- package/dist/sync/createIntentStream.d.ts +2 -1
- package/dist/sync/createIntentStream.js +46 -1
- package/dist/sync/participants.js +10 -16
- package/dist/transactions/TransactionQueue.js +13 -1
- package/dist/types/streams.d.ts +53 -33
- package/docs/api-keys.md +47 -3
- package/docs/api.md +103 -57
- package/docs/audit.md +16 -9
- package/docs/cli.md +222 -0
- package/docs/client-behavior.md +35 -21
- package/docs/coordination.md +74 -36
- package/docs/data-sources.md +23 -21
- package/docs/examples/agent-human.md +72 -28
- package/docs/examples/ai-sdk-tool.md +14 -11
- package/docs/examples/existing-python-backend.md +30 -19
- package/docs/examples/nextjs.md +21 -8
- package/docs/examples/scoped-agent.md +93 -0
- package/docs/examples/server-agent.md +27 -5
- package/docs/guarantees.md +29 -17
- package/docs/identity.md +198 -121
- package/docs/index.md +35 -18
- package/docs/integration-guide.md +79 -83
- package/docs/interaction-model.md +40 -25
- package/docs/mcp/claude-code.md +9 -17
- package/docs/mcp/cursor.md +6 -24
- package/docs/mcp/windsurf.md +6 -19
- package/docs/mcp.md +103 -26
- package/docs/quickstart.md +31 -39
- package/docs/react.md +18 -14
- package/docs/roadmap.md +15 -3
- package/docs/schema-contract.md +109 -0
- package/examples/README.md +8 -4
- package/examples/data-source/README.md +6 -2
- package/examples/data-source/run.ts +4 -3
- package/examples/quickstart.ts +1 -1
- package/llms.txt +27 -16
- package/package.json +13 -1
package/dist/errors.d.ts
CHANGED
|
@@ -18,27 +18,71 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Both work on every subclass.
|
|
20
20
|
*/
|
|
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';
|
|
21
24
|
/** Common shape for all errors thrown by this SDK. */
|
|
22
25
|
export declare class AbloError extends Error {
|
|
23
26
|
/** Discriminator string — matches the class name. Lets consumers
|
|
24
27
|
* switch on `e.type` without `instanceof` checks across package
|
|
25
28
|
* boundaries (matches Stripe's `err.type` pattern). */
|
|
26
29
|
readonly type: string;
|
|
27
|
-
/** Stable short identifier for logs + metrics
|
|
28
|
-
*
|
|
30
|
+
/** Stable short identifier for logs + metrics, drawn from the closed
|
|
31
|
+
* {@link ErrorCode} registry — e.g. `'apikey_invalid'`,
|
|
32
|
+
* `'capability_scope_denied'`. Stored as a plain `string` (not
|
|
33
|
+
* `ErrorCode`) so an older SDK still surfaces a newer server's code it
|
|
34
|
+
* doesn't recognise yet; producers are constrained at the constructor
|
|
35
|
+
* param instead. */
|
|
29
36
|
readonly code?: string;
|
|
30
37
|
/** HTTP status code when the error originated from an HTTP response. */
|
|
31
38
|
readonly httpStatus?: number;
|
|
32
39
|
/** Correlation id for ops — present when the server sent one on
|
|
33
40
|
* `x-request-id`. Include in support tickets. */
|
|
34
41
|
readonly requestId?: string;
|
|
42
|
+
/** Which input caused the error — a model/field path like
|
|
43
|
+
* `'dataroomMember.grants.subject'`. Mirrors Stripe's `error.param`;
|
|
44
|
+
* lets tooling point at the exact offending declaration. */
|
|
45
|
+
readonly param?: string;
|
|
46
|
+
/** Link to the docs for this `code`. Mirrors Stripe's `error.doc_url`.
|
|
47
|
+
* Defaults from `code` via {@link docUrlForCode} when omitted. */
|
|
48
|
+
readonly docUrl?: string;
|
|
49
|
+
/** Domain-specific structured payload merged into the wire envelope —
|
|
50
|
+
* e.g. a schema push's `{ warnings, unexecutable }`, a stale write's
|
|
51
|
+
* conflicting rows. Mirrors how Stripe attaches type-specific fields
|
|
52
|
+
* (`decline_code`, `payment_intent`) alongside the standard ones, so a
|
|
53
|
+
* structured error keeps its detail through `toJSON` instead of being
|
|
54
|
+
* flattened to a bare message. */
|
|
55
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
35
56
|
constructor(message: string, options?: {
|
|
36
|
-
code?:
|
|
57
|
+
code?: ErrorCode;
|
|
37
58
|
httpStatus?: number;
|
|
38
59
|
requestId?: string;
|
|
39
60
|
cause?: unknown;
|
|
61
|
+
param?: string;
|
|
62
|
+
docUrl?: string;
|
|
63
|
+
details?: Readonly<Record<string, unknown>>;
|
|
40
64
|
});
|
|
65
|
+
/**
|
|
66
|
+
* Serialize to Stripe's error-object shape: `{ type, code, param, message,
|
|
67
|
+
* doc_url, request_id }`. One JSON shape across HTTP bodies, WS frames, and
|
|
68
|
+
* logs — so consumers parse Ablo errors the way they already parse Stripe's.
|
|
69
|
+
*/
|
|
70
|
+
toJSON(): {
|
|
71
|
+
type: string;
|
|
72
|
+
code?: string;
|
|
73
|
+
param?: string;
|
|
74
|
+
message: string;
|
|
75
|
+
doc_url?: string;
|
|
76
|
+
request_id?: string;
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
};
|
|
41
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Map a stable error `code` to its docs URL — the one place the convention
|
|
82
|
+
* lives, so every error carrying a code gets a `doc_url` for free (Stripe
|
|
83
|
+
* ships a link on every error).
|
|
84
|
+
*/
|
|
85
|
+
export declare function docUrlForCode(code: ErrorCode): string;
|
|
42
86
|
/** 401 — invalid/missing/expired credentials. */
|
|
43
87
|
export declare class AbloAuthenticationError extends AbloError {
|
|
44
88
|
readonly type: "AbloAuthenticationError";
|
|
@@ -53,11 +97,12 @@ export declare class AbloRateLimitError extends AbloError {
|
|
|
53
97
|
readonly type: "AbloRateLimitError";
|
|
54
98
|
readonly retryAfterSeconds?: number;
|
|
55
99
|
constructor(message: string, options?: {
|
|
56
|
-
code?:
|
|
100
|
+
code?: ErrorCode;
|
|
57
101
|
httpStatus?: number;
|
|
58
102
|
requestId?: string;
|
|
59
103
|
cause?: unknown;
|
|
60
104
|
retryAfterSeconds?: number;
|
|
105
|
+
details?: Readonly<Record<string, unknown>>;
|
|
61
106
|
});
|
|
62
107
|
}
|
|
63
108
|
/** 409 — same `Idempotency-Key` reused with a different request body. */
|
|
@@ -98,7 +143,7 @@ export declare class AbloStaleContextError extends AbloError {
|
|
|
98
143
|
readonly observedSyncId: number;
|
|
99
144
|
}>;
|
|
100
145
|
constructor(message: string, options?: {
|
|
101
|
-
code?:
|
|
146
|
+
code?: ErrorCode;
|
|
102
147
|
httpStatus?: number;
|
|
103
148
|
requestId?: string;
|
|
104
149
|
cause?: unknown;
|
|
@@ -121,7 +166,7 @@ export declare class AbloClaimedError extends AbloError {
|
|
|
121
166
|
readonly type: "AbloClaimedError";
|
|
122
167
|
readonly claims?: ReadonlyArray<unknown>;
|
|
123
168
|
constructor(message: string, options?: {
|
|
124
|
-
code?:
|
|
169
|
+
code?: ErrorCode;
|
|
125
170
|
httpStatus?: number;
|
|
126
171
|
requestId?: string;
|
|
127
172
|
cause?: unknown;
|
|
@@ -227,11 +272,62 @@ export declare class SyncSessionError extends AbloAuthenticationError {
|
|
|
227
272
|
*/
|
|
228
273
|
static isSessionErrorResponse(status: number, body?: string): boolean;
|
|
229
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Coerce ANY thrown value into an {@link AbloError} — the last-line guarantee
|
|
277
|
+
* that an SDK consumer never catches an untagged error. An already-typed
|
|
278
|
+
* AbloError passes through untouched (so `code`/`httpStatus`/subclass survive);
|
|
279
|
+
* a bare `Error` keeps its message and is preserved as `cause` (carrying any
|
|
280
|
+
* `.code` someone attached); a non-Error is stringified.
|
|
281
|
+
*
|
|
282
|
+
* This is the client mirror of the server's `normalizeError` — applied at the
|
|
283
|
+
* SDK's public async boundaries so `instanceof AbloError` / `e.type` always
|
|
284
|
+
* hold for whatever a consumer catches, regardless of which internal layer
|
|
285
|
+
* (transport, IndexedDB, bootstrap, a third-party throw) produced it.
|
|
286
|
+
*/
|
|
287
|
+
export declare function toAbloError(err: unknown): AbloError;
|
|
288
|
+
/**
|
|
289
|
+
* Build the appropriate typed {@link AbloError} from a wire error — the
|
|
290
|
+
* single code→class mapping shared by every transport that can reject a
|
|
291
|
+
* request (HTTP responses via {@link translateHttpError}, WebSocket
|
|
292
|
+
* `mutation_result`/`claim_ack` frames, agent-job receipts).
|
|
293
|
+
*
|
|
294
|
+
* Code-first, then status-driven. A known {@link ErrorCode} carries its own
|
|
295
|
+
* canonical `httpStatus` in the registry, so frame transports that don't have
|
|
296
|
+
* an HTTP status (the WebSocket commit path) still produce the right subclass
|
|
297
|
+
* — instead of a hand-rolled `new Error(message)` that drops out of the typed
|
|
298
|
+
* hierarchy and loses `code`/`httpStatus`/retryability.
|
|
299
|
+
*/
|
|
300
|
+
export declare function errorFromWire(message: string, opts?: {
|
|
301
|
+
code?: string;
|
|
302
|
+
/** Explicit transport status (HTTP). When omitted, derived from the
|
|
303
|
+
* registry spec for `code` so frame transports map correctly too. */
|
|
304
|
+
httpStatus?: number;
|
|
305
|
+
requestId?: string;
|
|
306
|
+
requiredCapability?: RequiredCapability;
|
|
307
|
+
}): AbloError;
|
|
230
308
|
/**
|
|
231
309
|
* Translate an HTTP response into the appropriate typed error.
|
|
232
310
|
*
|
|
233
311
|
* Single source of truth for status-code → class mapping — every SDK
|
|
234
312
|
* fetch path that sees a non-2xx response should route through here
|
|
235
|
-
* so the customer-visible error is always the right subclass.
|
|
313
|
+
* so the customer-visible error is always the right subclass. Delegates
|
|
314
|
+
* the actual class selection to {@link errorFromWire} (shared with the
|
|
315
|
+
* frame transports) after extracting code/message from the HTTP body.
|
|
236
316
|
*/
|
|
237
317
|
export declare function translateHttpError(status: number, body: unknown, requestId?: string): AbloError;
|
|
318
|
+
/**
|
|
319
|
+
* Whether an HTTP error body carries a code {@link translateHttpError} can read
|
|
320
|
+
* — a top-level `code`, a nested `error.code`, or a string `error`. Callers that
|
|
321
|
+
* own a meaningful fallback code (e.g. `turn_open_failed`) use this to decide
|
|
322
|
+
* between routing through `translateHttpError` (structured envelope present) and
|
|
323
|
+
* throwing their own typed error with the fallback (bare/non-Ablo body), instead
|
|
324
|
+
* of emitting a code-less error.
|
|
325
|
+
*/
|
|
326
|
+
export declare function hasWireCode(body: unknown): boolean;
|
|
327
|
+
/**
|
|
328
|
+
* Extract the canonical error `code` from a raw HTTP error body STRING — the
|
|
329
|
+
* top-level `code` or a nested `error.code`. Returns undefined for non-JSON or
|
|
330
|
+
* code-less bodies. Used by session-error detection to tell a genuine expiry
|
|
331
|
+
* (`session_expired`/`jwt_expired`) apart from other auth failures.
|
|
332
|
+
*/
|
|
333
|
+
export declare function extractWireCode(body?: string): string | undefined;
|
package/dist/errors.js
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Both work on every subclass.
|
|
20
20
|
*/
|
|
21
|
+
import { errorCodeSpec } from './errorCodes.js';
|
|
22
|
+
export { ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode } from './errorCodes.js';
|
|
21
23
|
// ── AbloError hierarchy — the typed error surface ────────────────────
|
|
22
24
|
/** Common shape for all errors thrown by this SDK. */
|
|
23
25
|
export class AbloError extends Error {
|
|
@@ -25,14 +27,32 @@ export class AbloError extends Error {
|
|
|
25
27
|
* switch on `e.type` without `instanceof` checks across package
|
|
26
28
|
* boundaries (matches Stripe's `err.type` pattern). */
|
|
27
29
|
type = 'AbloError';
|
|
28
|
-
/** Stable short identifier for logs + metrics
|
|
29
|
-
*
|
|
30
|
+
/** Stable short identifier for logs + metrics, drawn from the closed
|
|
31
|
+
* {@link ErrorCode} registry — e.g. `'apikey_invalid'`,
|
|
32
|
+
* `'capability_scope_denied'`. Stored as a plain `string` (not
|
|
33
|
+
* `ErrorCode`) so an older SDK still surfaces a newer server's code it
|
|
34
|
+
* doesn't recognise yet; producers are constrained at the constructor
|
|
35
|
+
* param instead. */
|
|
30
36
|
code;
|
|
31
37
|
/** HTTP status code when the error originated from an HTTP response. */
|
|
32
38
|
httpStatus;
|
|
33
39
|
/** Correlation id for ops — present when the server sent one on
|
|
34
40
|
* `x-request-id`. Include in support tickets. */
|
|
35
41
|
requestId;
|
|
42
|
+
/** Which input caused the error — a model/field path like
|
|
43
|
+
* `'dataroomMember.grants.subject'`. Mirrors Stripe's `error.param`;
|
|
44
|
+
* lets tooling point at the exact offending declaration. */
|
|
45
|
+
param;
|
|
46
|
+
/** Link to the docs for this `code`. Mirrors Stripe's `error.doc_url`.
|
|
47
|
+
* Defaults from `code` via {@link docUrlForCode} when omitted. */
|
|
48
|
+
docUrl;
|
|
49
|
+
/** Domain-specific structured payload merged into the wire envelope —
|
|
50
|
+
* e.g. a schema push's `{ warnings, unexecutable }`, a stale write's
|
|
51
|
+
* conflicting rows. Mirrors how Stripe attaches type-specific fields
|
|
52
|
+
* (`decline_code`, `payment_intent`) alongside the standard ones, so a
|
|
53
|
+
* structured error keeps its detail through `toJSON` instead of being
|
|
54
|
+
* flattened to a bare message. */
|
|
55
|
+
details;
|
|
36
56
|
constructor(message, options) {
|
|
37
57
|
super(message);
|
|
38
58
|
this.name = this.constructor.name;
|
|
@@ -42,10 +62,41 @@ export class AbloError extends Error {
|
|
|
42
62
|
this.httpStatus = options.httpStatus;
|
|
43
63
|
if (options?.requestId !== undefined)
|
|
44
64
|
this.requestId = options.requestId;
|
|
65
|
+
if (options?.param !== undefined)
|
|
66
|
+
this.param = options.param;
|
|
67
|
+
if (options?.details !== undefined)
|
|
68
|
+
this.details = options.details;
|
|
69
|
+
const docUrl = options?.docUrl ?? (options?.code ? docUrlForCode(options.code) : undefined);
|
|
70
|
+
if (docUrl !== undefined)
|
|
71
|
+
this.docUrl = docUrl;
|
|
45
72
|
if (options?.cause !== undefined) {
|
|
46
73
|
Object.defineProperty(this, 'cause', { value: options.cause, enumerable: false });
|
|
47
74
|
}
|
|
48
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Serialize to Stripe's error-object shape: `{ type, code, param, message,
|
|
78
|
+
* doc_url, request_id }`. One JSON shape across HTTP bodies, WS frames, and
|
|
79
|
+
* logs — so consumers parse Ablo errors the way they already parse Stripe's.
|
|
80
|
+
*/
|
|
81
|
+
toJSON() {
|
|
82
|
+
return {
|
|
83
|
+
type: this.type,
|
|
84
|
+
...(this.code !== undefined ? { code: this.code } : {}),
|
|
85
|
+
...(this.param !== undefined ? { param: this.param } : {}),
|
|
86
|
+
message: this.message,
|
|
87
|
+
...(this.docUrl !== undefined ? { doc_url: this.docUrl } : {}),
|
|
88
|
+
...(this.requestId !== undefined ? { request_id: this.requestId } : {}),
|
|
89
|
+
...(this.details ?? {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Map a stable error `code` to its docs URL — the one place the convention
|
|
95
|
+
* lives, so every error carrying a code gets a `doc_url` for free (Stripe
|
|
96
|
+
* ships a link on every error).
|
|
97
|
+
*/
|
|
98
|
+
export function docUrlForCode(code) {
|
|
99
|
+
return `https://docs.abloatai.com/errors#${code}`;
|
|
49
100
|
}
|
|
50
101
|
/** 401 — invalid/missing/expired credentials. */
|
|
51
102
|
export class AbloAuthenticationError extends AbloError {
|
|
@@ -187,29 +238,106 @@ export class SyncSessionError extends AbloAuthenticationError {
|
|
|
187
238
|
* Check if an HTTP response status indicates a session error
|
|
188
239
|
*/
|
|
189
240
|
static isSessionErrorResponse(status, body) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return true;
|
|
241
|
+
// Prefer the structured error code: ONLY a genuine session / JWT EXPIRY
|
|
242
|
+
// should drive sign-out + re-auth. A non-expiry auth failure
|
|
243
|
+
// (api_key_required, jwt_issuer_untrusted, a 403 permission denial, …) must
|
|
244
|
+
// NOT — re-authenticating re-mints the same credential and loops, which is
|
|
245
|
+
// exactly the "flash then bounce to /signin" symptom. This mirrors the
|
|
246
|
+
// canonical wire mapping (401 → Authentication, 403 → Permission).
|
|
247
|
+
const code = extractWireCode(body);
|
|
248
|
+
if (code) {
|
|
249
|
+
return code === 'session_expired' || code === 'jwt_expired';
|
|
203
250
|
}
|
|
204
|
-
|
|
251
|
+
// No structured code (bare body, non-Ablo proxy response): a 401 is taken as
|
|
252
|
+
// expiry — the historical default that drives re-auth — while a 403 is a
|
|
253
|
+
// permission failure, not a session error.
|
|
254
|
+
return status === 401;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Coerce ANY thrown value into an {@link AbloError} — the last-line guarantee
|
|
259
|
+
* that an SDK consumer never catches an untagged error. An already-typed
|
|
260
|
+
* AbloError passes through untouched (so `code`/`httpStatus`/subclass survive);
|
|
261
|
+
* a bare `Error` keeps its message and is preserved as `cause` (carrying any
|
|
262
|
+
* `.code` someone attached); a non-Error is stringified.
|
|
263
|
+
*
|
|
264
|
+
* This is the client mirror of the server's `normalizeError` — applied at the
|
|
265
|
+
* SDK's public async boundaries so `instanceof AbloError` / `e.type` always
|
|
266
|
+
* hold for whatever a consumer catches, regardless of which internal layer
|
|
267
|
+
* (transport, IndexedDB, bootstrap, a third-party throw) produced it.
|
|
268
|
+
*/
|
|
269
|
+
export function toAbloError(err) {
|
|
270
|
+
if (err instanceof AbloError)
|
|
271
|
+
return err;
|
|
272
|
+
if (err instanceof Error) {
|
|
273
|
+
const rawCode = err.code;
|
|
274
|
+
const code = typeof rawCode === 'string' ? rawCode : undefined;
|
|
275
|
+
return new AbloError(err.message, { code, cause: err });
|
|
276
|
+
}
|
|
277
|
+
return new AbloError(String(err), { cause: err });
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Build the appropriate typed {@link AbloError} from a wire error — the
|
|
281
|
+
* single code→class mapping shared by every transport that can reject a
|
|
282
|
+
* request (HTTP responses via {@link translateHttpError}, WebSocket
|
|
283
|
+
* `mutation_result`/`claim_ack` frames, agent-job receipts).
|
|
284
|
+
*
|
|
285
|
+
* Code-first, then status-driven. A known {@link ErrorCode} carries its own
|
|
286
|
+
* canonical `httpStatus` in the registry, so frame transports that don't have
|
|
287
|
+
* an HTTP status (the WebSocket commit path) still produce the right subclass
|
|
288
|
+
* — instead of a hand-rolled `new Error(message)` that drops out of the typed
|
|
289
|
+
* hierarchy and loses `code`/`httpStatus`/retryability.
|
|
290
|
+
*/
|
|
291
|
+
export function errorFromWire(message, opts = {}) {
|
|
292
|
+
const { code, requestId, requiredCapability } = opts;
|
|
293
|
+
// Effective status: an explicit HTTP status wins; otherwise fall back to
|
|
294
|
+
// the code's canonical status from the registry (undefined for unknown /
|
|
295
|
+
// forward-compat codes, which then map to the base AbloError).
|
|
296
|
+
const httpStatus = opts.httpStatus ?? (code ? errorCodeSpec(code)?.httpStatus : undefined);
|
|
297
|
+
// Wire boundary: an incoming code is an arbitrary string (a newer server
|
|
298
|
+
// may send a code this SDK predates). Cast to ErrorCode here — the one
|
|
299
|
+
// sanctioned crossing — so internal producers stay statically checked.
|
|
300
|
+
const publicCode = (code === 'intent_conflict' ? 'claim_conflict' : code);
|
|
301
|
+
const baseOpts = { code: publicCode, httpStatus, requestId };
|
|
302
|
+
// ── Code-first specials (transport-independent) ──────────────────────
|
|
303
|
+
// A scoped credential was denied — route through CapabilityError so callers
|
|
304
|
+
// can read `.requiredCapability` to attenuate-and-retry.
|
|
305
|
+
if (code === 'capability_scope_denied' || code === 'capability_invalid') {
|
|
306
|
+
return new CapabilityError(code, message, requiredCapability);
|
|
307
|
+
}
|
|
308
|
+
// Claim enforcement (rides 409): the target entity is held by another
|
|
309
|
+
// participant. Discriminate on code BEFORE the generic 409→idempotency
|
|
310
|
+
// mapping so a claim rejection surfaces as AbloClaimedError.
|
|
311
|
+
if (code === 'intent_conflict' || code === 'claim_conflict' || code === 'entity_claimed') {
|
|
312
|
+
return new AbloClaimedError(message, baseOpts);
|
|
313
|
+
}
|
|
314
|
+
// A write whose `readAt` watermark went stale — callers re-read and retry.
|
|
315
|
+
if (code === 'stale_context') {
|
|
316
|
+
return new AbloStaleContextError(message, baseOpts);
|
|
205
317
|
}
|
|
318
|
+
// ── Status-driven dispatch (HTTP parity) ─────────────────────────────
|
|
319
|
+
if (httpStatus === 401)
|
|
320
|
+
return new AbloAuthenticationError(message, baseOpts);
|
|
321
|
+
if (httpStatus === 403)
|
|
322
|
+
return new AbloPermissionError(message, baseOpts);
|
|
323
|
+
if (httpStatus === 409)
|
|
324
|
+
return new AbloIdempotencyError(message, baseOpts);
|
|
325
|
+
if (httpStatus === 422 || httpStatus === 400)
|
|
326
|
+
return new AbloValidationError(message, baseOpts);
|
|
327
|
+
if (httpStatus === 429)
|
|
328
|
+
return new AbloRateLimitError(message, baseOpts);
|
|
329
|
+
if (httpStatus !== undefined && httpStatus >= 500)
|
|
330
|
+
return new AbloServerError(message, baseOpts);
|
|
331
|
+
return new AbloError(message, baseOpts);
|
|
206
332
|
}
|
|
207
333
|
/**
|
|
208
334
|
* Translate an HTTP response into the appropriate typed error.
|
|
209
335
|
*
|
|
210
336
|
* Single source of truth for status-code → class mapping — every SDK
|
|
211
337
|
* fetch path that sees a non-2xx response should route through here
|
|
212
|
-
* so the customer-visible error is always the right subclass.
|
|
338
|
+
* so the customer-visible error is always the right subclass. Delegates
|
|
339
|
+
* the actual class selection to {@link errorFromWire} (shared with the
|
|
340
|
+
* frame transports) after extracting code/message from the HTTP body.
|
|
213
341
|
*/
|
|
214
342
|
export function translateHttpError(status, body, requestId) {
|
|
215
343
|
const parsed = typeof body === 'object' && body !== null ? body : {};
|
|
@@ -224,30 +352,53 @@ export function translateHttpError(status, body, requestId) {
|
|
|
224
352
|
flatError ??
|
|
225
353
|
(typeof body === 'string' ? body : `HTTP ${status}`);
|
|
226
354
|
const requiredCapability = nested?.requiredCapability ?? parsed.requiredCapability;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
355
|
+
return errorFromWire(message, { code, httpStatus: status, requestId, requiredCapability });
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Whether an HTTP error body carries a code {@link translateHttpError} can read
|
|
359
|
+
* — a top-level `code`, a nested `error.code`, or a string `error`. Callers that
|
|
360
|
+
* own a meaningful fallback code (e.g. `turn_open_failed`) use this to decide
|
|
361
|
+
* between routing through `translateHttpError` (structured envelope present) and
|
|
362
|
+
* throwing their own typed error with the fallback (bare/non-Ablo body), instead
|
|
363
|
+
* of emitting a code-less error.
|
|
364
|
+
*/
|
|
365
|
+
export function hasWireCode(body) {
|
|
366
|
+
if (typeof body !== 'object' || body === null)
|
|
367
|
+
return false;
|
|
368
|
+
const b = body;
|
|
369
|
+
if (typeof b.code === 'string')
|
|
370
|
+
return true;
|
|
371
|
+
if (typeof b.error === 'string')
|
|
372
|
+
return true;
|
|
373
|
+
return (typeof b.error === 'object' &&
|
|
374
|
+
b.error !== null &&
|
|
375
|
+
typeof b.error.code === 'string');
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Extract the canonical error `code` from a raw HTTP error body STRING — the
|
|
379
|
+
* top-level `code` or a nested `error.code`. Returns undefined for non-JSON or
|
|
380
|
+
* code-less bodies. Used by session-error detection to tell a genuine expiry
|
|
381
|
+
* (`session_expired`/`jwt_expired`) apart from other auth failures.
|
|
382
|
+
*/
|
|
383
|
+
export function extractWireCode(body) {
|
|
384
|
+
if (!body)
|
|
385
|
+
return undefined;
|
|
386
|
+
let parsed;
|
|
387
|
+
try {
|
|
388
|
+
parsed = JSON.parse(body);
|
|
236
389
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// claim rejection surfaces as AbloClaimedError, not AbloIdempotencyError —
|
|
240
|
-
// same typed error the WebSocket commit path yields for these codes.
|
|
241
|
-
if (code === 'intent_conflict' || code === 'claim_conflict' || code === 'entity_claimed') {
|
|
242
|
-
return new AbloClaimedError(message, baseOpts);
|
|
390
|
+
catch {
|
|
391
|
+
return undefined;
|
|
243
392
|
}
|
|
244
|
-
if (
|
|
245
|
-
return
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
393
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
394
|
+
return undefined;
|
|
395
|
+
const b = parsed;
|
|
396
|
+
if (typeof b.code === 'string')
|
|
397
|
+
return b.code;
|
|
398
|
+
if (typeof b.error === 'object' &&
|
|
399
|
+
b.error !== null &&
|
|
400
|
+
typeof b.error.code === 'string') {
|
|
401
|
+
return b.error.code;
|
|
402
|
+
}
|
|
403
|
+
return undefined;
|
|
253
404
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* import Ablo from '@abloatai/ablo';
|
|
6
6
|
*
|
|
7
7
|
* const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
8
|
-
* await ablo.weatherReports.
|
|
8
|
+
* const report = await ablo.weatherReports.retrieve('report_stockholm');
|
|
9
9
|
* await ablo.weatherReports.update('report_stockholm', { status: 'ready' });
|
|
10
10
|
*
|
|
11
11
|
* type Entry = Ablo.Peer;
|
|
@@ -23,9 +23,13 @@
|
|
|
23
23
|
* @abloatai/ablo/react — <AbloProvider>, useQuery, useMutate
|
|
24
24
|
* @abloatai/ablo/testing — test harnesses + mocks
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Reads split by where the data comes from. `ablo.<model>.retrieve(id)` and
|
|
27
|
+
* `.list({ where })` are the async **server** reads (pool → IDB → network via
|
|
28
|
+
* the `HydrationCoordinator`, single-flight deduped); they're the default and
|
|
29
|
+
* what hosted/stateless callers want, since their local graph starts empty.
|
|
30
|
+
* `ablo.<model>.get(id)` / `.getAll(...)` / `.getCount(...)` are synchronous
|
|
31
|
+
* **local-graph** snapshots with no network round-trip — for reactive React
|
|
32
|
+
* selectors (`useAblo((ablo) => ablo.<model>.get(id))`) once the graph is warm.
|
|
29
33
|
*
|
|
30
34
|
* ── What to import (read this first) ────────────────────────────────
|
|
31
35
|
* Default path — this is all most apps and agents ever need:
|
|
@@ -48,9 +52,10 @@ export { session, agent } from './principal.js';
|
|
|
48
52
|
import { Ablo } from './client/Ablo.js';
|
|
49
53
|
export default Ablo;
|
|
50
54
|
export { dataSource, abloSource, signAbloSourceRequest, verifyAbloSourceRequest, } from './source/index.js';
|
|
51
|
-
export { defaultPolicy } from './policy/index.js';
|
|
52
|
-
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, } from './errors.js';
|
|
55
|
+
export { defaultPolicy, capabilityPreemptPolicy } from './policy/index.js';
|
|
56
|
+
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, } from './errors.js';
|
|
53
57
|
export type { CommitReceipt, RequiredCapability } from './errors.js';
|
|
58
|
+
export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec } from './errors.js';
|
|
54
59
|
export type { Register, DefaultSyncShape } from './types/global.js';
|
|
55
60
|
export { defineMutators } from './mutators/defineMutators.js';
|
|
56
61
|
export { createTransaction, type Transaction } from './mutators/Transaction.js';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* import Ablo from '@abloatai/ablo';
|
|
6
6
|
*
|
|
7
7
|
* const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
8
|
-
* await ablo.weatherReports.
|
|
8
|
+
* const report = await ablo.weatherReports.retrieve('report_stockholm');
|
|
9
9
|
* await ablo.weatherReports.update('report_stockholm', { status: 'ready' });
|
|
10
10
|
*
|
|
11
11
|
* type Entry = Ablo.Peer;
|
|
@@ -23,9 +23,13 @@
|
|
|
23
23
|
* @abloatai/ablo/react — <AbloProvider>, useQuery, useMutate
|
|
24
24
|
* @abloatai/ablo/testing — test harnesses + mocks
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Reads split by where the data comes from. `ablo.<model>.retrieve(id)` and
|
|
27
|
+
* `.list({ where })` are the async **server** reads (pool → IDB → network via
|
|
28
|
+
* the `HydrationCoordinator`, single-flight deduped); they're the default and
|
|
29
|
+
* what hosted/stateless callers want, since their local graph starts empty.
|
|
30
|
+
* `ablo.<model>.get(id)` / `.getAll(...)` / `.getCount(...)` are synchronous
|
|
31
|
+
* **local-graph** snapshots with no network round-trip — for reactive React
|
|
32
|
+
* selectors (`useAblo((ablo) => ablo.<model>.get(id))`) once the graph is warm.
|
|
29
33
|
*
|
|
30
34
|
* ── What to import (read this first) ────────────────────────────────
|
|
31
35
|
* Default path — this is all most apps and agents ever need:
|
|
@@ -74,11 +78,11 @@ export { dataSource, abloSource, signAbloSourceRequest, verifyAbloSourceRequest,
|
|
|
74
78
|
// (reject-on-stale) is already applied server-side, so you only import it
|
|
75
79
|
// to COMPOSE a custom policy. Leave it alone and stale writes are rejected
|
|
76
80
|
// safely by default. Type counterparts live under `Ablo.Conflict.*`.
|
|
77
|
-
export { defaultPolicy } from './policy/index.js';
|
|
81
|
+
export { defaultPolicy, capabilityPreemptPolicy } from './policy/index.js';
|
|
78
82
|
// Typed error hierarchy — Stripe-style. One import gets every class
|
|
79
83
|
// consumers need to discriminate failures (`e instanceof AbloX` or
|
|
80
84
|
// `e.type === 'AbloX'`) plus the HTTP-response translator.
|
|
81
|
-
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, } from './errors.js';
|
|
85
|
+
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, } from './errors.js';
|
|
82
86
|
// Advanced — most apps never import this. Custom (Zero-style) mutators:
|
|
83
87
|
// `ablo.<model>.create/update/delete` already covers normal writes. Reach
|
|
84
88
|
// for `defineMutators` only when you need a named, multi-step mutation with
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Ablo API-key format — the single source of truth for how keys
|
|
3
|
+
* are minted, hashed, and validated. Both the sync-server (`apiKeyStore`)
|
|
4
|
+
* and the web control-plane (`generate-key.ts`) consume THIS module, so the
|
|
5
|
+
* format can no longer drift between the two mint sites (it used to live as
|
|
6
|
+
* a hand-copied twin kept in sync by a comment).
|
|
7
|
+
*
|
|
8
|
+
* Node-only — uses `node:crypto`. Exposed via the `@abloatai/ablo/keys`
|
|
9
|
+
* subpath and NEVER re-exported from the browser-facing `.` entry, so the
|
|
10
|
+
* client bundle never pulls in `node:crypto`.
|
|
11
|
+
*
|
|
12
|
+
* Format (GitHub-style): `<sk|rk|ek>_<live|test>_<30 base62 body><6-char
|
|
13
|
+
* base62 CRC32 checksum>`. The identifiable prefix + CRC32 checksum let
|
|
14
|
+
* secret scanners detect leaks and let us reject typo'd/forged keys OFFLINE
|
|
15
|
+
* (no DB round-trip). Legacy keys (a ~43-char base64url body, no checksum)
|
|
16
|
+
* still validate by hash — they parse here as `checksummed: false`.
|
|
17
|
+
*/
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
export declare const API_KEY_KINDS: readonly ["secret", "restricted", "ephemeral"];
|
|
20
|
+
export type ApiKeyKind = (typeof API_KEY_KINDS)[number];
|
|
21
|
+
export declare const API_KEY_ENVS: readonly ["live", "test"];
|
|
22
|
+
export type ApiKeyEnv = (typeof API_KEY_ENVS)[number];
|
|
23
|
+
/** A structurally-valid Ablo API key, parsed into its parts. */
|
|
24
|
+
export interface ParsedApiKey {
|
|
25
|
+
/** The original plaintext. */
|
|
26
|
+
raw: string;
|
|
27
|
+
kind: ApiKeyKind;
|
|
28
|
+
env: ApiKeyEnv;
|
|
29
|
+
/** The chars after `<prefix>_<env>_` (body + checksum for new keys). */
|
|
30
|
+
body: string;
|
|
31
|
+
/** True when this is the new checksummed format (36-char base62 body). */
|
|
32
|
+
checksummed: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Canonical schema for an Ablo API key. `parse`/`safeParse` returns a typed
|
|
36
|
+
* {@link ParsedApiKey}; a new checksummed-format key with a BAD checksum is
|
|
37
|
+
* rejected (the offline-reject), while a legacy key parses as
|
|
38
|
+
* `checksummed: false` and passes (the server still hash-validates it).
|
|
39
|
+
*/
|
|
40
|
+
export declare const apiKeySchema: z.ZodPipe<z.ZodString, z.ZodTransform<ParsedApiKey, string>>;
|
|
41
|
+
/** Parse + fully validate (incl. checksum). Returns null when invalid. */
|
|
42
|
+
export declare function parseApiKey(raw: string): ParsedApiKey | null;
|
|
43
|
+
/** True when the key uses the new checksummed format (regardless of validity). */
|
|
44
|
+
export declare function isChecksummedKey(raw: string): boolean;
|
|
45
|
+
/** Verify the embedded checksum. Meaningful only for checksummed-format keys. */
|
|
46
|
+
export declare function keyChecksumMatches(raw: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Mint a key: `<prefix>_<env>_<body><checksum>`. Returns the plaintext (shown
|
|
49
|
+
* once), its SHA-256 hash (persisted), and the 12-char display prefix.
|
|
50
|
+
*/
|
|
51
|
+
export declare function generateApiKey(env?: ApiKeyEnv, kind?: ApiKeyKind): {
|
|
52
|
+
plaintext: string;
|
|
53
|
+
hash: string;
|
|
54
|
+
prefix: string;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Stable SHA-256 hex of a plaintext key. A fast hash is CORRECT here (not
|
|
58
|
+
* bcrypt) — API keys are high-entropy random, so there's no dictionary to
|
|
59
|
+
* defend against. Used at both write (mint) and lookup.
|
|
60
|
+
*/
|
|
61
|
+
export declare function hashApiKey(plaintext: string): string;
|