@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.
Files changed (181) hide show
  1. package/CHANGELOG.md +72 -1
  2. package/README.md +80 -66
  3. package/dist/BaseSyncedStore.d.ts +73 -0
  4. package/dist/BaseSyncedStore.js +179 -5
  5. package/dist/Model.d.ts +42 -0
  6. package/dist/Model.js +103 -44
  7. package/dist/SyncEngineContext.d.ts +2 -1
  8. package/dist/SyncEngineContext.js +5 -3
  9. package/dist/agent/session.js +6 -5
  10. package/dist/ai-sdk/coordination-context.js +4 -0
  11. package/dist/ai-sdk/index.d.ts +56 -47
  12. package/dist/ai-sdk/index.js +56 -47
  13. package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
  14. package/dist/ai-sdk/intent-broadcast.js +11 -4
  15. package/dist/ai-sdk/wrap.d.ts +14 -11
  16. package/dist/ai-sdk/wrap.js +11 -13
  17. package/dist/auth/credentialSource.d.ts +34 -0
  18. package/dist/auth/credentialSource.js +63 -0
  19. package/dist/auth/index.d.ts +2 -22
  20. package/dist/auth/index.js +26 -36
  21. package/dist/auth/schemas.d.ts +35 -0
  22. package/dist/auth/schemas.js +53 -0
  23. package/dist/client/Ablo.d.ts +259 -33
  24. package/dist/client/Ablo.js +276 -73
  25. package/dist/client/ApiClient.d.ts +52 -4
  26. package/dist/client/ApiClient.js +236 -66
  27. package/dist/client/auth.d.ts +21 -2
  28. package/dist/client/auth.js +77 -5
  29. package/dist/client/createInternalComponents.d.ts +2 -0
  30. package/dist/client/createInternalComponents.js +8 -1
  31. package/dist/client/createModelProxy.d.ts +187 -79
  32. package/dist/client/createModelProxy.js +203 -68
  33. package/dist/client/httpClient.d.ts +71 -0
  34. package/dist/client/httpClient.js +69 -0
  35. package/dist/client/identity.d.ts +2 -6
  36. package/dist/client/identity.js +63 -11
  37. package/dist/client/index.d.ts +1 -0
  38. package/dist/client/index.js +1 -0
  39. package/dist/client/registerDataSource.d.ts +19 -0
  40. package/dist/client/registerDataSource.js +59 -0
  41. package/dist/client/validateAbloOptions.d.ts +2 -1
  42. package/dist/client/validateAbloOptions.js +8 -7
  43. package/dist/core/DatabaseManager.js +30 -2
  44. package/dist/core/openIDBWithTimeout.d.ts +36 -0
  45. package/dist/core/openIDBWithTimeout.js +88 -1
  46. package/dist/errorCodes.d.ts +92 -1
  47. package/dist/errorCodes.js +139 -7
  48. package/dist/errors.d.ts +54 -3
  49. package/dist/errors.js +192 -44
  50. package/dist/index.d.ts +23 -10
  51. package/dist/index.js +21 -8
  52. package/dist/keys/index.d.ts +76 -0
  53. package/dist/keys/index.js +171 -0
  54. package/dist/mutators/UndoManager.d.ts +86 -50
  55. package/dist/mutators/UndoManager.js +129 -22
  56. package/dist/mutators/inverseOp.d.ts +129 -0
  57. package/dist/mutators/inverseOp.js +74 -0
  58. package/dist/mutators/readerActions.d.ts +1 -1
  59. package/dist/mutators/undoApply.d.ts +42 -0
  60. package/dist/mutators/undoApply.js +143 -0
  61. package/dist/query/client.d.ts +10 -9
  62. package/dist/query/client.js +22 -14
  63. package/dist/react/AbloProvider.d.ts +23 -101
  64. package/dist/react/AbloProvider.js +61 -103
  65. package/dist/react/ClientSideSuspense.d.ts +1 -1
  66. package/dist/react/DefaultFallback.d.ts +1 -1
  67. package/dist/react/SyncGroupProvider.d.ts +1 -1
  68. package/dist/react/index.d.ts +3 -2
  69. package/dist/react/index.js +3 -2
  70. package/dist/react/useAblo.d.ts +4 -4
  71. package/dist/react/useAblo.js +10 -5
  72. package/dist/react/useCurrentUserId.d.ts +1 -1
  73. package/dist/react/useCurrentUserId.js +1 -1
  74. package/dist/react/useMutators.js +19 -12
  75. package/dist/react/useReactive.js +16 -3
  76. package/dist/schema/ddl.d.ts +26 -3
  77. package/dist/schema/ddl.js +152 -4
  78. package/dist/schema/index.d.ts +4 -0
  79. package/dist/schema/index.js +12 -0
  80. package/dist/schema/model.d.ts +11 -0
  81. package/dist/schema/model.js +2 -0
  82. package/dist/schema/openapi.d.ts +28 -0
  83. package/dist/schema/openapi.js +118 -0
  84. package/dist/schema/plane.d.ts +23 -0
  85. package/dist/schema/plane.js +19 -0
  86. package/dist/schema/relation.d.ts +20 -0
  87. package/dist/schema/serialize.d.ts +7 -3
  88. package/dist/schema/serialize.js +6 -2
  89. package/dist/schema/sync-delta-row.d.ts +157 -0
  90. package/dist/schema/sync-delta-row.js +102 -0
  91. package/dist/schema/sync-delta-wire.d.ts +180 -0
  92. package/dist/schema/sync-delta-wire.js +102 -0
  93. package/dist/server/adapter.d.ts +156 -0
  94. package/dist/server/adapter.js +19 -0
  95. package/dist/server/commit.d.ts +82 -0
  96. package/dist/server/commit.js +1 -0
  97. package/dist/server/index.d.ts +14 -0
  98. package/dist/server/index.js +1 -0
  99. package/dist/server/next.d.ts +51 -0
  100. package/dist/server/next.js +47 -0
  101. package/dist/server/read-config.d.ts +60 -0
  102. package/dist/server/read-config.js +8 -0
  103. package/dist/server/storage-mode.d.ts +17 -0
  104. package/dist/server/storage-mode.js +12 -0
  105. package/dist/source/adapter.d.ts +59 -0
  106. package/dist/source/adapter.js +19 -0
  107. package/dist/source/adapters/drizzle.d.ts +34 -0
  108. package/dist/source/adapters/drizzle.js +147 -0
  109. package/dist/source/adapters/memory.d.ts +12 -0
  110. package/dist/source/adapters/memory.js +114 -0
  111. package/dist/source/adapters/prisma.d.ts +57 -0
  112. package/dist/source/adapters/prisma.js +199 -0
  113. package/dist/source/conformance.d.ts +32 -0
  114. package/dist/source/conformance.js +134 -0
  115. package/dist/source/contract.d.ts +143 -0
  116. package/dist/source/contract.js +98 -0
  117. package/dist/source/index.d.ts +61 -10
  118. package/dist/source/index.js +98 -0
  119. package/dist/source/next.d.ts +33 -0
  120. package/dist/source/next.js +26 -0
  121. package/dist/sync/BootstrapHelper.d.ts +10 -0
  122. package/dist/sync/BootstrapHelper.js +56 -42
  123. package/dist/sync/ConnectionManager.d.ts +57 -1
  124. package/dist/sync/ConnectionManager.js +186 -11
  125. package/dist/sync/HydrationCoordinator.d.ts +93 -17
  126. package/dist/sync/HydrationCoordinator.js +241 -41
  127. package/dist/sync/NetworkProbe.d.ts +60 -18
  128. package/dist/sync/NetworkProbe.js +121 -23
  129. package/dist/sync/SyncWebSocket.d.ts +45 -70
  130. package/dist/sync/SyncWebSocket.js +113 -89
  131. package/dist/sync/createIntentStream.js +10 -1
  132. package/dist/sync/participants.js +5 -2
  133. package/dist/transactions/TransactionQueue.js +13 -1
  134. package/dist/types/streams.d.ts +9 -0
  135. package/dist/utils/mobx-setup.js +1 -0
  136. package/dist/webhooks/events.d.ts +38 -0
  137. package/dist/webhooks/events.js +40 -0
  138. package/dist/webhooks/index.d.ts +10 -0
  139. package/dist/webhooks/index.js +10 -0
  140. package/dist/wire/errorEnvelope.d.ts +34 -0
  141. package/dist/wire/errorEnvelope.js +86 -0
  142. package/dist/wire/frames.d.ts +119 -0
  143. package/dist/wire/frames.js +1 -0
  144. package/dist/wire/index.d.ts +24 -0
  145. package/dist/wire/index.js +21 -0
  146. package/dist/wire/listEnvelope.d.ts +45 -0
  147. package/dist/wire/listEnvelope.js +17 -0
  148. package/docs/api-keys.md +5 -5
  149. package/docs/api.md +125 -65
  150. package/docs/audit.md +16 -9
  151. package/docs/cli.md +57 -47
  152. package/docs/client-behavior.md +54 -40
  153. package/docs/coordination.md +66 -80
  154. package/docs/data-sources.md +56 -34
  155. package/docs/examples/agent-human.md +74 -28
  156. package/docs/examples/ai-sdk-tool.md +29 -22
  157. package/docs/examples/existing-python-backend.md +41 -26
  158. package/docs/examples/nextjs.md +32 -17
  159. package/docs/examples/scoped-agent.md +43 -28
  160. package/docs/examples/server-agent.md +40 -15
  161. package/docs/guarantees.md +38 -27
  162. package/docs/identity.md +65 -59
  163. package/docs/index.md +30 -19
  164. package/docs/integration-guide.md +78 -78
  165. package/docs/interaction-model.md +43 -35
  166. package/docs/mcp/claude-code.md +11 -19
  167. package/docs/mcp/cursor.md +7 -25
  168. package/docs/mcp/windsurf.md +7 -20
  169. package/docs/mcp.md +103 -26
  170. package/docs/quickstart.md +63 -61
  171. package/docs/react.md +24 -16
  172. package/docs/roadmap.md +13 -13
  173. package/docs/schema-contract.md +111 -0
  174. package/docs/the-loop.md +21 -0
  175. package/examples/README.md +8 -4
  176. package/examples/data-source/README.md +10 -7
  177. package/examples/data-source/customer-server.ts +27 -25
  178. package/examples/data-source/run.ts +4 -3
  179. package/examples/quickstart.ts +1 -1
  180. package/llms.txt +55 -21
  181. package/package.json +48 -3
@@ -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-05-28';
40
- const wire = (category, httpStatus, retryable, message) => ({ category, surface: 'wire', httpStatus, retryable, message });
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
- apikey_expired: wire('auth', 401, false, 'API key has expired.'),
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
- session_expired: wire('auth', 401, false, 'The session is invalid or expired; re-authenticate.'),
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 bring-your-own DB role cannot enforce row-level security.'),
63
- byo_role_unreadable: wire('permission', 403, false, 'The bring-your-own DB role could not be introspected.'),
64
- byo_tenant_tables_unforced_rls: wire('permission', 403, false, 'Tenant tables do not have RLS forced under the BYO role.'),
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
- export { ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode } from './errorCodes.js';
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
- if (status === 401)
241
- return true;
242
- if (status === 403) {
243
- if (body) {
244
- const lowerBody = body.toLowerCase();
245
- if (lowerBody.includes('session') ||
246
- lowerBody.includes('unauthorized') ||
247
- lowerBody.includes('not authenticated') ||
248
- lowerBody.includes('token')) {
249
- return true;
250
- }
251
- }
252
- return true;
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
- return false;
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 = typeof body === 'object' && body !== null ? body : {};
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
- // Wire boundary: an incoming code is an arbitrary string (a newer server
278
- // may send a code this SDK predates). Cast to ErrorCode here — the one
279
- // sanctioned crossing — so internal producers stay statically checked.
280
- const publicCode = (code === 'intent_conflict' ? 'claim_conflict' : code);
281
- const baseOpts = { code: publicCode, httpStatus: status, requestId };
282
- if (status === 401)
283
- return new AbloAuthenticationError(message, baseOpts);
284
- if (status === 403 || code === 'capability_scope_denied' || code === 'capability_invalid') {
285
- if (code === 'capability_scope_denied' || code === 'capability_invalid') {
286
- return new CapabilityError(code, message, requiredCapability);
287
- }
288
- return new AbloPermissionError(message, baseOpts);
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
- // Claim enforcement also rides 409 (a commit blocked by a foreign claim).
291
- // Discriminate on the code BEFORE the generic idempotency mapping so a
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 (status === 409)
298
- return new AbloIdempotencyError(message, baseOpts);
299
- if (status === 422 || status === 400)
300
- return new AbloValidationError(message, baseOpts);
301
- if (status === 429)
302
- return new AbloRateLimitError(message, baseOpts);
303
- if (status >= 500)
304
- return new AbloServerError(message, baseOpts);
305
- return new AbloError(message, baseOpts);
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.load({ where: { id: 'report_stockholm' } });
9
- * await ablo.weatherReports.update('report_stockholm', { status: 'ready' });
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
- * Consumer code should converge on `ablo.<model>.load(...)`, which routes
27
- * through the engine's `HydrationCoordinator` and dedupes single-flight
28
- * hydrations.
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*Options` bags
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 { AbloOptions, ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ClaimOptions, ClaimedRow, ModelOperations, } from './client/Ablo.js';
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';