@abloatai/ablo 0.7.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 +72 -1
- package/README.md +80 -66
- package/dist/BaseSyncedStore.d.ts +73 -0
- package/dist/BaseSyncedStore.js +179 -5
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/SyncEngineContext.d.ts +2 -1
- package/dist/SyncEngineContext.js +5 -3
- package/dist/agent/session.js +6 -5
- 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 +26 -36
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +259 -33
- package/dist/client/Ablo.js +276 -73
- package/dist/client/ApiClient.d.ts +52 -4
- package/dist/client/ApiClient.js +236 -66
- package/dist/client/auth.d.ts +21 -2
- package/dist/client/auth.js +77 -5
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +187 -79
- package/dist/client/createModelProxy.js +203 -68
- 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 +63 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +19 -0
- package/dist/client/registerDataSource.js +59 -0
- package/dist/client/validateAbloOptions.d.ts +2 -1
- package/dist/client/validateAbloOptions.js +8 -7
- 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 +92 -1
- package/dist/errorCodes.js +139 -7
- package/dist/errors.d.ts +54 -3
- package/dist/errors.js +192 -44
- package/dist/index.d.ts +23 -10
- package/dist/index.js +21 -8
- package/dist/keys/index.d.ts +76 -0
- package/dist/keys/index.js +171 -0
- 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 +22 -14
- package/dist/react/AbloProvider.d.ts +23 -101
- package/dist/react/AbloProvider.js +61 -103
- 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/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/react/useReactive.js +16 -3
- 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 +7 -3
- package/dist/schema/serialize.js +6 -2
- 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 +56 -42
- package/dist/sync/ConnectionManager.d.ts +57 -1
- package/dist/sync/ConnectionManager.js +186 -11
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +241 -41
- package/dist/sync/NetworkProbe.d.ts +60 -18
- package/dist/sync/NetworkProbe.js +121 -23
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +113 -89
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/sync/participants.js +5 -2
- package/dist/transactions/TransactionQueue.js +13 -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-keys.md +5 -5
- package/docs/api.md +125 -65
- package/docs/audit.md +16 -9
- package/docs/cli.md +57 -47
- package/docs/client-behavior.md +54 -40
- package/docs/coordination.md +66 -80
- package/docs/data-sources.md +56 -34
- package/docs/examples/agent-human.md +74 -28
- package/docs/examples/ai-sdk-tool.md +29 -22
- package/docs/examples/existing-python-backend.md +41 -26
- package/docs/examples/nextjs.md +32 -17
- package/docs/examples/scoped-agent.md +43 -28
- package/docs/examples/server-agent.md +40 -15
- package/docs/guarantees.md +38 -27
- package/docs/identity.md +65 -59
- package/docs/index.md +30 -19
- package/docs/integration-guide.md +78 -78
- package/docs/interaction-model.md +43 -35
- package/docs/mcp/claude-code.md +11 -19
- package/docs/mcp/cursor.md +7 -25
- package/docs/mcp/windsurf.md +7 -20
- package/docs/mcp.md +103 -26
- package/docs/quickstart.md +63 -61
- package/docs/react.md +24 -16
- package/docs/roadmap.md +13 -13
- package/docs/schema-contract.md +111 -0
- package/docs/the-loop.md +21 -0
- package/examples/README.md +8 -4
- package/examples/data-source/README.md +10 -7
- package/examples/data-source/customer-server.ts +27 -25
- package/examples/data-source/run.ts +4 -3
- package/examples/quickstart.ts +1 -1
- package/llms.txt +55 -21
- package/package.json +48 -3
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,21 +85,53 @@ 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.'),
|
|
54
98
|
exchange_failed: wire('auth', 401, false, 'The API-key credential exchange was rejected.'),
|
|
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>`.'),
|
|
101
|
+
identity_missing_organization: wire('auth', 401, false, 'Authentication succeeded but resolved to no organization context.'),
|
|
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'),
|
|
104
|
+
// `jwt_invalid` is the residual fallback; the codes below split out the
|
|
105
|
+
// specific failure modes so an integrating customer can tell "I registered
|
|
106
|
+
// the wrong JWKS" from "my token has no org claim" from "wrong audience"
|
|
107
|
+
// rather than getting one opaque code for all of them.
|
|
108
|
+
jwt_invalid: wire('auth', 401, false, 'The bearer JWT could not be validated (unclassified).'),
|
|
109
|
+
jwt_malformed: wire('auth', 401, false, 'The bearer JWT is not a well-formed JWT and could not be decoded.'),
|
|
110
|
+
jwt_missing_issuer: wire('auth', 401, false, 'The bearer JWT has no `iss` (issuer) claim, so it cannot be routed to a trusted issuer.'),
|
|
111
|
+
jwt_issuer_untrusted: wire('auth', 401, false, "The bearer JWT's `iss` is not a registered trusted issuer. Register it via POST /v1/trusted-issuers, or check the token's issuer claim."),
|
|
112
|
+
jwt_signature_invalid: wire('auth', 401, false, "The bearer JWT's signature could not be verified against the issuer's JWKS (wrong key, rotated key, or forged token)."),
|
|
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."),
|
|
114
|
+
jwt_missing_subject: wire('auth', 401, false, 'The bearer JWT has no `sub` (subject) claim to identify the user.'),
|
|
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.'),
|
|
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'),
|
|
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)."),
|
|
57
123
|
file_upload_auth_required: wire('auth', 401, false, 'File upload requires an authenticated session.'),
|
|
58
124
|
browser_apikey_blocked: client('auth', 'Raw API keys must not be used from a browser context.'),
|
|
125
|
+
browser_database_url_blocked: client('auth', 'A database connection string must not be used from a browser context — it carries DB credentials.'),
|
|
126
|
+
datasource_registration_failed: client('auth', 'Failed to register the provided databaseUrl for the direct Postgres connector.'),
|
|
59
127
|
// ── permission / capability (403) ──────────────────────────────────
|
|
60
128
|
capability_scope_denied: wire('capability', 403, false, "The connection's resolved scope does not cover the attempted action."),
|
|
129
|
+
issuer_register_forbidden: wire('permission', 403, false, 'Registering a trusted issuer requires a secret (sk_) API key.'),
|
|
61
130
|
capability_invalid: wire('capability', 403, false, 'The capability is unknown, revoked, or expired.'),
|
|
62
|
-
byo_role_cannot_enforce_rls: wire('permission', 403, false, 'The
|
|
63
|
-
byo_role_unreadable: wire('permission', 403, false, 'The
|
|
64
|
-
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.'),
|
|
65
135
|
// ── claim / intent conflict (409) ──────────────────────────────────
|
|
66
136
|
claim_conflict: wire('claim', 409, true, 'The target entity is claimed by another participant.'),
|
|
67
137
|
claim_lost: wire('claim', 409, true, 'A previously held claim was lost before the write applied.'),
|
|
@@ -80,6 +150,7 @@ export const ERROR_CODES = {
|
|
|
80
150
|
commit_operation_required: wire('validation', 400, false, 'A commit must carry `operation` or `operations`.'),
|
|
81
151
|
commit_operation_model_required: wire('validation', 400, false, 'A commit operation is missing its `model`.'),
|
|
82
152
|
commit_operations_ambiguous: wire('validation', 400, false, 'A commit supplied both `operation` and `operations`.'),
|
|
153
|
+
commit_too_many_operations: wire('validation', 400, false, 'A commit exceeded the per-commit operation limit; split it into smaller batches.'),
|
|
83
154
|
model_required_field_missing: wire('validation', 400, false, 'A required field was absent from the model payload.'),
|
|
84
155
|
model_identifier_missing: wire('validation', 400, false, 'The model payload is missing its identifier.'),
|
|
85
156
|
snapshot_reserved_key: wire('validation', 400, false, 'A snapshot used a reserved key name.'),
|
|
@@ -92,6 +163,18 @@ export const ERROR_CODES = {
|
|
|
92
163
|
model_not_found: wire('not_found', 404, false, 'The referenced model row does not exist.'),
|
|
93
164
|
mutate_update_entity_not_found: wire('not_found', 404, false, 'The entity targeted by an update does not exist.'),
|
|
94
165
|
task_id_missing: wire('server', 502, true, 'The task-create response did not include an id.'),
|
|
166
|
+
// ── data integrity / DB constraints ────────────────────────────────
|
|
167
|
+
// Emitted when a write is rejected by a database integrity constraint
|
|
168
|
+
// (Postgres class-23). All NON-retryable: the same payload re-sent
|
|
169
|
+
// unchanged will fail identically, so the client must roll back, not
|
|
170
|
+
// retry. The server normalizer maps SQLSTATE → these codes and tucks the
|
|
171
|
+
// raw constraint/column/table detail into `details` rather than leaking
|
|
172
|
+
// the driver's message text onto the wire.
|
|
173
|
+
not_null_violation: wire('validation', 400, false, 'A required field was missing (database not-null constraint).'),
|
|
174
|
+
foreign_key_violation: wire('conflict', 409, false, 'A referenced entity does not exist, or is still referenced (database foreign-key constraint).'),
|
|
175
|
+
unique_violation: wire('conflict', 409, false, 'A value violates a uniqueness constraint.'),
|
|
176
|
+
check_violation: wire('validation', 400, false, 'A value violates a database check constraint.'),
|
|
177
|
+
constraint_violation: wire('validation', 400, false, 'A database integrity constraint was violated.'),
|
|
95
178
|
// ── tenant / unknown model (400) ───────────────────────────────────
|
|
96
179
|
server_execute_unknown_model: wire('tenant', 400, false, 'The server-execute request named a model not in the tenant schema.'),
|
|
97
180
|
mutate_create_unknown_model: wire('tenant', 400, false, 'A create targeted a model not in the tenant schema.'),
|
|
@@ -140,18 +223,21 @@ export const ERROR_CODES = {
|
|
|
140
223
|
queue_too_deep: wire('transport', 503, true, 'The transaction queue exceeded its depth limit.'),
|
|
141
224
|
flush_timeout: wire('transport', 504, true, 'Timed out flushing the transaction queue.'),
|
|
142
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.'),
|
|
143
227
|
fetch_unavailable: client('transport', 'No fetch implementation is available in this environment.'),
|
|
144
228
|
base_url_missing: client('transport', 'No base URL was configured for the client.'),
|
|
145
229
|
sync_not_ready: client('transport', 'A sync operation was attempted before the client was ready.'),
|
|
146
230
|
ws_not_ready: client('transport', 'A frame was sent before the WebSocket was connected.'),
|
|
147
231
|
// ── quota / rate limit (429) ──────────────────────────────────────
|
|
148
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.'),
|
|
149
234
|
// ── server (5xx) ───────────────────────────────────────────────────
|
|
150
235
|
internal_error: wire('server', 500, true, 'An unexpected server error occurred.'),
|
|
151
236
|
quota_lookup_failed: wire('server', 503, true, 'The quota decision could not be loaded.'),
|
|
152
237
|
turn_open_failed: wire('server', 500, true, 'The agent turn failed to open.'),
|
|
153
238
|
turn_close_failed: wire('server', 500, true, 'The agent turn failed to close cleanly.'),
|
|
154
239
|
// ── client-only invariants (never serialized) ──────────────────────
|
|
240
|
+
invalid_options: client('client', 'The Ablo client was constructed with invalid or incomplete options.'),
|
|
155
241
|
no_ablo_provider: client('client', 'An Ablo hook was used outside of an Ablo provider.'),
|
|
156
242
|
no_sync_group_provider: client('client', 'A sync-group hook was used outside of its provider.'),
|
|
157
243
|
sync_context_missing_provider: client('client', 'Sync context was read outside of its provider.'),
|
|
@@ -186,6 +272,7 @@ export const ERROR_CODES = {
|
|
|
186
272
|
mutator_registry_unnamed_def: client('client', 'A mutator definition was registered without a name.'),
|
|
187
273
|
mutators_schema_missing: client('client', 'Mutators were registered without a schema.'),
|
|
188
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.'),
|
|
189
276
|
mock_mutation_failed: client('client', 'A mock mutation adapter was configured to fail.'),
|
|
190
277
|
mock_unsupported_operation: client('client', 'A mock adapter received an unsupported operation.'),
|
|
191
278
|
// ── HTTP route edge codes (egress through app.onError) ─────────────
|
|
@@ -249,3 +336,48 @@ export function errorCodeSpec(code) {
|
|
|
249
336
|
export function isRetryableCode(code) {
|
|
250
337
|
return errorCodeSpec(code)?.retryable ?? false;
|
|
251
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
|
|
@@ -272,11 +272,62 @@ export declare class SyncSessionError extends AbloAuthenticationError {
|
|
|
272
272
|
*/
|
|
273
273
|
static isSessionErrorResponse(status: number, body?: string): boolean;
|
|
274
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;
|
|
275
308
|
/**
|
|
276
309
|
* Translate an HTTP response into the appropriate typed error.
|
|
277
310
|
*
|
|
278
311
|
* Single source of truth for status-code → class mapping — every SDK
|
|
279
312
|
* fetch path that sees a non-2xx response should route through here
|
|
280
|
-
* 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.
|
|
281
316
|
*/
|
|
282
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,7 +18,9 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Both work on every subclass.
|
|
20
20
|
*/
|
|
21
|
-
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import { errorCodeSpec, classifyRecovery } from './errorCodes.js';
|
|
23
|
+
export { ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, classifyRecovery, recoveryClassSchema, RECOVERY_CLASSES, } from './errorCodes.js';
|
|
22
24
|
// ── AbloError hierarchy — the typed error surface ────────────────────
|
|
23
25
|
/** Common shape for all errors thrown by this SDK. */
|
|
24
26
|
export class AbloError extends Error {
|
|
@@ -237,32 +239,160 @@ export class SyncSessionError extends AbloAuthenticationError {
|
|
|
237
239
|
* Check if an HTTP response status indicates a session error
|
|
238
240
|
*/
|
|
239
241
|
static isSessionErrorResponse(status, body) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
242
|
+
// "Should this response sign the user out?" — TRUE only for a genuine
|
|
243
|
+
// expiry of the LONG-LIVED login (`recovery: 'session_expiry'`). Decided
|
|
244
|
+
// via the closed recovery taxonomy rather than a hardcoded code list, so
|
|
245
|
+
// the access-vs-session split lives in one place (errorCodes.ts). This is
|
|
246
|
+
// behaviourally identical to the old `session_expired || jwt_expired` list.
|
|
247
|
+
//
|
|
248
|
+
// Deliberately NOT true for `access_credential_expiry` (`apikey_expired` —
|
|
249
|
+
// the Stripe-style ephemeral key): an expired `ek_`/`rk_` is re-mintable
|
|
250
|
+
// from the still-valid login and must NOT log the user out — the connection
|
|
251
|
+
// layer silently re-mints instead. Likewise NOT true for `auth_blocked` /
|
|
252
|
+
// `permission` failures (api_key_required, jwt_issuer_untrusted, 403s):
|
|
253
|
+
// re-auth re-mints the same rejected credential and loops ("flash then
|
|
254
|
+
// bounce to /signin").
|
|
255
|
+
const code = extractWireCode(body);
|
|
256
|
+
if (code) {
|
|
257
|
+
return classifyRecovery(code) === 'session_expiry';
|
|
253
258
|
}
|
|
254
|
-
|
|
259
|
+
// No structured code (bare body, non-Ablo proxy response): a 401 is taken as
|
|
260
|
+
// expiry — the historical default that drives re-auth — while a 403 is a
|
|
261
|
+
// permission failure, not a session error.
|
|
262
|
+
return status === 401;
|
|
255
263
|
}
|
|
256
264
|
}
|
|
265
|
+
// ── HTTP → class mapping ──────────────────────────────────────────────
|
|
266
|
+
const OptionalWireStringSchema = z.preprocess((value) => (typeof value === 'string' ? value : undefined), z.string().optional());
|
|
267
|
+
const RequiredCapabilityWireSchema = z
|
|
268
|
+
.object({
|
|
269
|
+
scope: z.string(),
|
|
270
|
+
constraints: z
|
|
271
|
+
.record(z.string(), z.union([z.array(z.string()), z.string()]))
|
|
272
|
+
.optional(),
|
|
273
|
+
issuer: OptionalWireStringSchema,
|
|
274
|
+
ttlSeconds: z
|
|
275
|
+
.preprocess((value) => (typeof value === 'number' ? value : undefined), z.number().optional()),
|
|
276
|
+
nonce: OptionalWireStringSchema,
|
|
277
|
+
})
|
|
278
|
+
.passthrough();
|
|
279
|
+
const NestedErrorShapeSchema = z
|
|
280
|
+
.object({
|
|
281
|
+
code: OptionalWireStringSchema,
|
|
282
|
+
message: OptionalWireStringSchema,
|
|
283
|
+
field: OptionalWireStringSchema,
|
|
284
|
+
requiredCapability: RequiredCapabilityWireSchema.optional().catch(undefined),
|
|
285
|
+
})
|
|
286
|
+
.passthrough();
|
|
287
|
+
const ErrorFieldSchema = z
|
|
288
|
+
.preprocess((value) => typeof value === 'string' || (typeof value === 'object' && value !== null)
|
|
289
|
+
? value
|
|
290
|
+
: undefined, z.union([z.string(), NestedErrorShapeSchema]).optional())
|
|
291
|
+
.catch(undefined);
|
|
292
|
+
const ErrorBodyShapeSchema = z
|
|
293
|
+
.object({
|
|
294
|
+
/** Legacy: `error` was a flat code string on older endpoints. Newer
|
|
295
|
+
* endpoints (CommitReceipt) carry `error` as a nested object. */
|
|
296
|
+
error: ErrorFieldSchema,
|
|
297
|
+
code: OptionalWireStringSchema,
|
|
298
|
+
reason: OptionalWireStringSchema,
|
|
299
|
+
message: OptionalWireStringSchema,
|
|
300
|
+
requiredCapability: RequiredCapabilityWireSchema.optional().catch(undefined),
|
|
301
|
+
})
|
|
302
|
+
.passthrough();
|
|
303
|
+
function parseErrorBodyShape(body) {
|
|
304
|
+
if (typeof body !== 'object' || body === null)
|
|
305
|
+
return {};
|
|
306
|
+
const parsed = ErrorBodyShapeSchema.safeParse(body);
|
|
307
|
+
return parsed.success ? parsed.data : {};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Coerce ANY thrown value into an {@link AbloError} — the last-line guarantee
|
|
311
|
+
* that an SDK consumer never catches an untagged error. An already-typed
|
|
312
|
+
* AbloError passes through untouched (so `code`/`httpStatus`/subclass survive);
|
|
313
|
+
* a bare `Error` keeps its message and is preserved as `cause` (carrying any
|
|
314
|
+
* `.code` someone attached); a non-Error is stringified.
|
|
315
|
+
*
|
|
316
|
+
* This is the client mirror of the server's `normalizeError` — applied at the
|
|
317
|
+
* SDK's public async boundaries so `instanceof AbloError` / `e.type` always
|
|
318
|
+
* hold for whatever a consumer catches, regardless of which internal layer
|
|
319
|
+
* (transport, IndexedDB, bootstrap, a third-party throw) produced it.
|
|
320
|
+
*/
|
|
321
|
+
export function toAbloError(err) {
|
|
322
|
+
if (err instanceof AbloError)
|
|
323
|
+
return err;
|
|
324
|
+
if (err instanceof Error) {
|
|
325
|
+
const rawCode = err.code;
|
|
326
|
+
const code = typeof rawCode === 'string' ? rawCode : undefined;
|
|
327
|
+
return new AbloError(err.message, { code, cause: err });
|
|
328
|
+
}
|
|
329
|
+
return new AbloError(String(err), { cause: err });
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Build the appropriate typed {@link AbloError} from a wire error — the
|
|
333
|
+
* single code→class mapping shared by every transport that can reject a
|
|
334
|
+
* request (HTTP responses via {@link translateHttpError}, WebSocket
|
|
335
|
+
* `mutation_result`/`claim_ack` frames, agent-job receipts).
|
|
336
|
+
*
|
|
337
|
+
* Code-first, then status-driven. A known {@link ErrorCode} carries its own
|
|
338
|
+
* canonical `httpStatus` in the registry, so frame transports that don't have
|
|
339
|
+
* an HTTP status (the WebSocket commit path) still produce the right subclass
|
|
340
|
+
* — instead of a hand-rolled `new Error(message)` that drops out of the typed
|
|
341
|
+
* hierarchy and loses `code`/`httpStatus`/retryability.
|
|
342
|
+
*/
|
|
343
|
+
export function errorFromWire(message, opts = {}) {
|
|
344
|
+
const { code, requestId, requiredCapability } = opts;
|
|
345
|
+
// Effective status: an explicit HTTP status wins; otherwise fall back to
|
|
346
|
+
// the code's canonical status from the registry (undefined for unknown /
|
|
347
|
+
// forward-compat codes, which then map to the base AbloError).
|
|
348
|
+
const httpStatus = opts.httpStatus ?? (code ? errorCodeSpec(code)?.httpStatus : undefined);
|
|
349
|
+
// Wire boundary: an incoming code is an arbitrary string (a newer server
|
|
350
|
+
// may send a code this SDK predates). Cast to ErrorCode here — the one
|
|
351
|
+
// sanctioned crossing — so internal producers stay statically checked.
|
|
352
|
+
const publicCode = (code === 'intent_conflict' ? 'claim_conflict' : code);
|
|
353
|
+
const baseOpts = { code: publicCode, httpStatus, requestId };
|
|
354
|
+
// ── Code-first specials (transport-independent) ──────────────────────
|
|
355
|
+
// A scoped credential was denied — route through CapabilityError so callers
|
|
356
|
+
// can read `.requiredCapability` to attenuate-and-retry.
|
|
357
|
+
if (code === 'capability_scope_denied' || code === 'capability_invalid') {
|
|
358
|
+
return new CapabilityError(code, message, requiredCapability);
|
|
359
|
+
}
|
|
360
|
+
// Claim enforcement (rides 409): the target entity is held by another
|
|
361
|
+
// participant. Discriminate on code BEFORE the generic 409→idempotency
|
|
362
|
+
// mapping so a claim rejection surfaces as AbloClaimedError.
|
|
363
|
+
if (code === 'intent_conflict' || code === 'claim_conflict' || code === 'entity_claimed') {
|
|
364
|
+
return new AbloClaimedError(message, baseOpts);
|
|
365
|
+
}
|
|
366
|
+
// A write whose `readAt` watermark went stale — callers re-read and retry.
|
|
367
|
+
if (code === 'stale_context') {
|
|
368
|
+
return new AbloStaleContextError(message, baseOpts);
|
|
369
|
+
}
|
|
370
|
+
// ── Status-driven dispatch (HTTP parity) ─────────────────────────────
|
|
371
|
+
if (httpStatus === 401)
|
|
372
|
+
return new AbloAuthenticationError(message, baseOpts);
|
|
373
|
+
if (httpStatus === 403)
|
|
374
|
+
return new AbloPermissionError(message, baseOpts);
|
|
375
|
+
if (httpStatus === 409)
|
|
376
|
+
return new AbloIdempotencyError(message, baseOpts);
|
|
377
|
+
if (httpStatus === 422 || httpStatus === 400)
|
|
378
|
+
return new AbloValidationError(message, baseOpts);
|
|
379
|
+
if (httpStatus === 429)
|
|
380
|
+
return new AbloRateLimitError(message, baseOpts);
|
|
381
|
+
if (httpStatus !== undefined && httpStatus >= 500)
|
|
382
|
+
return new AbloServerError(message, baseOpts);
|
|
383
|
+
return new AbloError(message, baseOpts);
|
|
384
|
+
}
|
|
257
385
|
/**
|
|
258
386
|
* Translate an HTTP response into the appropriate typed error.
|
|
259
387
|
*
|
|
260
388
|
* Single source of truth for status-code → class mapping — every SDK
|
|
261
389
|
* fetch path that sees a non-2xx response should route through here
|
|
262
|
-
* so the customer-visible error is always the right subclass.
|
|
390
|
+
* so the customer-visible error is always the right subclass. Delegates
|
|
391
|
+
* the actual class selection to {@link errorFromWire} (shared with the
|
|
392
|
+
* frame transports) after extracting code/message from the HTTP body.
|
|
263
393
|
*/
|
|
264
394
|
export function translateHttpError(status, body, requestId) {
|
|
265
|
-
const parsed =
|
|
395
|
+
const parsed = parseErrorBodyShape(body);
|
|
266
396
|
const nested = parsed.error != null && typeof parsed.error === 'object'
|
|
267
397
|
? parsed.error
|
|
268
398
|
: undefined;
|
|
@@ -274,33 +404,51 @@ export function translateHttpError(status, body, requestId) {
|
|
|
274
404
|
flatError ??
|
|
275
405
|
(typeof body === 'string' ? body : `HTTP ${status}`);
|
|
276
406
|
const requiredCapability = nested?.requiredCapability ?? parsed.requiredCapability;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
407
|
+
return errorFromWire(message, { code, httpStatus: status, requestId, requiredCapability });
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Whether an HTTP error body carries a code {@link translateHttpError} can read
|
|
411
|
+
* — a top-level `code`, a nested `error.code`, or a string `error`. Callers that
|
|
412
|
+
* own a meaningful fallback code (e.g. `turn_open_failed`) use this to decide
|
|
413
|
+
* between routing through `translateHttpError` (structured envelope present) and
|
|
414
|
+
* throwing their own typed error with the fallback (bare/non-Ablo body), instead
|
|
415
|
+
* of emitting a code-less error.
|
|
416
|
+
*/
|
|
417
|
+
export function hasWireCode(body) {
|
|
418
|
+
const parsed = parseErrorBodyShape(body);
|
|
419
|
+
if (typeof parsed.code === 'string')
|
|
420
|
+
return true;
|
|
421
|
+
if (typeof parsed.error === 'string')
|
|
422
|
+
return true;
|
|
423
|
+
return (typeof parsed.error === 'object' &&
|
|
424
|
+
parsed.error !== null &&
|
|
425
|
+
typeof parsed.error.code === 'string');
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Extract the canonical error `code` from a raw HTTP error body STRING — the
|
|
429
|
+
* top-level `code` or a nested `error.code`. Returns undefined for non-JSON or
|
|
430
|
+
* code-less bodies. Used by session-error detection to tell a genuine expiry
|
|
431
|
+
* (`session_expired`/`jwt_expired`) apart from other auth failures.
|
|
432
|
+
*/
|
|
433
|
+
export function extractWireCode(body) {
|
|
434
|
+
if (!body)
|
|
435
|
+
return undefined;
|
|
436
|
+
let parsed;
|
|
437
|
+
try {
|
|
438
|
+
parsed = JSON.parse(body);
|
|
289
439
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// claim rejection surfaces as AbloClaimedError, not AbloIdempotencyError —
|
|
293
|
-
// same typed error the WebSocket commit path yields for these codes.
|
|
294
|
-
if (code === 'intent_conflict' || code === 'claim_conflict' || code === 'entity_claimed') {
|
|
295
|
-
return new AbloClaimedError(message, baseOpts);
|
|
440
|
+
catch {
|
|
441
|
+
return undefined;
|
|
296
442
|
}
|
|
297
|
-
if (
|
|
298
|
-
return
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
443
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
444
|
+
return undefined;
|
|
445
|
+
const b = parseErrorBodyShape(parsed);
|
|
446
|
+
if (typeof b.code === 'string')
|
|
447
|
+
return b.code;
|
|
448
|
+
if (typeof b.error === 'string')
|
|
449
|
+
return b.error;
|
|
450
|
+
if (typeof b.error === 'object' && b.error !== null && typeof b.error.code === 'string') {
|
|
451
|
+
return b.error.code;
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
306
454
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
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.
|
|
9
|
-
* await ablo.weatherReports.update(
|
|
8
|
+
* const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
|
|
9
|
+
* await ablo.weatherReports.update({
|
|
10
|
+
* id: 'report_stockholm',
|
|
11
|
+
* data: { status: 'ready' },
|
|
12
|
+
* });
|
|
10
13
|
*
|
|
11
14
|
* type Entry = Ablo.Peer;
|
|
12
15
|
* ```
|
|
@@ -23,13 +26,17 @@
|
|
|
23
26
|
* @abloatai/ablo/react — <AbloProvider>, useQuery, useMutate
|
|
24
27
|
* @abloatai/ablo/testing — test harnesses + mocks
|
|
25
28
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
+
* Reads split by where the data comes from. `ablo.<model>.retrieve({ id })` and
|
|
30
|
+
* `.list({ where })` are the async **server** reads (pool → IDB → network via
|
|
31
|
+
* the `HydrationCoordinator`, single-flight deduped); they're the default and
|
|
32
|
+
* what hosted/stateless callers want, since their local graph starts empty.
|
|
33
|
+
* `ablo.<model>.get(id)` / `.getAll(...)` / `.getCount(...)` are synchronous
|
|
34
|
+
* **local-graph** snapshots with no network round-trip — for reactive React
|
|
35
|
+
* selectors (`useAblo((ablo) => ablo.<model>.get(id))`) once the graph is warm.
|
|
29
36
|
*
|
|
30
37
|
* ── What to import (read this first) ────────────────────────────────
|
|
31
38
|
* Default path — this is all most apps and agents ever need:
|
|
32
|
-
* • `Ablo` (default export) + `AbloOptions` + the `Model*
|
|
39
|
+
* • `Ablo` (default export) + `AbloOptions` + the `Model*Params` bags
|
|
33
40
|
* • the `Ablo*Error` classes, to discriminate failures in catch blocks
|
|
34
41
|
* That's it. If you're reaching past those, you're in advanced territory.
|
|
35
42
|
*
|
|
@@ -42,16 +49,22 @@
|
|
|
42
49
|
* If you don't recognize one, you don't need it — the default path covers you.
|
|
43
50
|
*/
|
|
44
51
|
export { Ablo } from './client/Ablo.js';
|
|
45
|
-
export type {
|
|
52
|
+
export type { MutationExecutor } from './interfaces/index.js';
|
|
53
|
+
export type { HttpClaimApi, InternalAbloOptions } from './client/Ablo.js';
|
|
54
|
+
export { createAbloHttpClient, type AbloHttpClientOptions, type AbloHttpClient, type HttpModelClient, } from './client/httpClient.js';
|
|
55
|
+
export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './client/auth.js';
|
|
56
|
+
export type { AbloOptions, ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, ClaimHandle, ModelOperations, } from './client/Ablo.js';
|
|
46
57
|
export type { AbloPersistence } from './client/persistence.js';
|
|
47
58
|
export { session, agent } from './principal.js';
|
|
48
59
|
import { Ablo } from './client/Ablo.js';
|
|
49
60
|
export default Ablo;
|
|
50
|
-
export { dataSource, abloSource, signAbloSourceRequest, verifyAbloSourceRequest, } from './source/index.js';
|
|
61
|
+
export { dataSource, abloSource, sourceEventForOperation, signAbloSourceRequest, verifyAbloSourceRequest, } from './source/index.js';
|
|
51
62
|
export { defaultPolicy, capabilityPreemptPolicy } from './policy/index.js';
|
|
52
|
-
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, } from './errors.js';
|
|
63
|
+
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, classifyRecovery, recoveryClassSchema, RECOVERY_CLASSES, } from './errors.js';
|
|
53
64
|
export type { CommitReceipt, RequiredCapability } from './errors.js';
|
|
54
|
-
export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec } from './errors.js';
|
|
65
|
+
export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec, RecoveryClass } from './errors.js';
|
|
66
|
+
export { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL } from './auth/credentialSource.js';
|
|
67
|
+
export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
|
|
55
68
|
export type { Register, DefaultSyncShape } from './types/global.js';
|
|
56
69
|
export { defineMutators } from './mutators/defineMutators.js';
|
|
57
70
|
export { createTransaction, type Transaction } from './mutators/Transaction.js';
|