@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
@@ -30,6 +30,7 @@
30
30
  * ```
31
31
  */
32
32
  export { Ablo, computeFKDepthPriority, type AbloOptions, type InternalAbloOptions, type ClaimedOptions, type IfClaimedPolicy, type IntentWaitOptions, type ModelCountOptions, type ModelListOptions, type ModelListScope, type ModelLoadOptions, type ModelOperations, type ModelReadOptions, } from './Ablo.js';
33
+ export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './auth.js';
33
34
  export type { AbloPersistence } from './persistence.js';
34
35
  export type { AbloApi, AbloApiClientOptions, AbloApiIntents, Agent, AgentIntentInput, AgentIntentOptions, AgentOptions, AgentModelClient, AgentModelReadOptions, AgentModelMutationOptions, AgentRunContext, AgentRunDone, AgentRunFailed, AgentRunCancelled, AgentRunOptions, AgentRunResult, AgentRunStatus, Capability, CapabilityCreateOptions, CapabilityParticipantKind, CapabilityRecord, CapabilityResource, CapabilityRevocation, CapabilityScope, Task, TaskCloseOptions, TaskCloseResult, TaskCreateOptions, TaskResource, } from './ApiClient.js';
35
36
  export type { EngineParticipant, JoinedParticipant, ParticipantJoinOptions, ParticipantManager, ParticipantScope, ParticipantStatus, ScopedIntents, ScopedPresence, } from '../sync/participants.js';
@@ -30,3 +30,4 @@
30
30
  * ```
31
31
  */
32
32
  export { Ablo, computeFKDepthPriority, } from './Ablo.js';
33
+ export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './auth.js';
@@ -0,0 +1,19 @@
1
+ export interface RegisterDataSourceInput {
2
+ /** HTTP API base, e.g. `https://api.abloatai.com/api` (from resolveBootstrapBaseUrl). */
3
+ readonly baseUrl: string;
4
+ /** Secret key (`sk_…`) used to authenticate + derive the org. */
5
+ readonly apiKey: string | null;
6
+ /** Postgres connection string for the direct connector. */
7
+ readonly databaseUrl: string;
8
+ /** Optional Postgres schema (defaults server-side to `public`). */
9
+ readonly schema?: string;
10
+ /** Custom fetch (tests/proxies/odd runtimes). */
11
+ readonly fetchImpl?: typeof fetch;
12
+ }
13
+ /**
14
+ * POST the connection string to the self-serve direct connector route. Resolves
15
+ * on success (the org is now a dedicated tenant pointed at this DB); throws an
16
+ * `AbloError` with `datasource_registration_failed` otherwise so `ready()`
17
+ * surfaces it instead of silently bootstrapping against the wrong store.
18
+ */
19
+ export declare function registerDataSource(input: RegisterDataSourceInput): Promise<void>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Self-serve direct-Postgres connector registration.
3
+ *
4
+ * Historical note: this module name says "DataSource", but this path registers
5
+ * a direct database URL. It is not the signed `dataSource(...)` endpoint path.
6
+ *
7
+ * When a client is constructed with `databaseUrl`, the SDK registers that
8
+ * connection string BEFORE bootstrap so the server resolves the org's data plane
9
+ * to that direct connector.
10
+ *
11
+ * The org is derived server-side from the API key — the caller never sends an
12
+ * organization id. The connection string is sent once over TLS and is never
13
+ * echoed back (the server stores it as a secret and returns only a safe
14
+ * `datasource` projection: host, database, schema).
15
+ */
16
+ import { AbloError } from '../errors.js';
17
+ /**
18
+ * POST the connection string to the self-serve direct connector route. Resolves
19
+ * on success (the org is now a dedicated tenant pointed at this DB); throws an
20
+ * `AbloError` with `datasource_registration_failed` otherwise so `ready()`
21
+ * surfaces it instead of silently bootstrapping against the wrong store.
22
+ */
23
+ export async function registerDataSource(input) {
24
+ if (!input.apiKey) {
25
+ throw new AbloError('databaseUrl requires an apiKey to register the direct Postgres connector (the org is derived from the key).', { code: 'datasource_registration_failed' });
26
+ }
27
+ const doFetch = input.fetchImpl ?? fetch;
28
+ const endpoint = `${input.baseUrl.replace(/\/+$/, '')}/v1/datasource`;
29
+ let response;
30
+ try {
31
+ response = await doFetch(endpoint, {
32
+ method: 'POST',
33
+ headers: {
34
+ 'content-type': 'application/json',
35
+ authorization: `Bearer ${input.apiKey}`,
36
+ },
37
+ body: JSON.stringify({
38
+ connectionString: input.databaseUrl,
39
+ ...(input.schema ? { schema: input.schema } : {}),
40
+ }),
41
+ });
42
+ }
43
+ catch (cause) {
44
+ throw new AbloError('Could not reach the Ablo API to register the direct Postgres connector.', {
45
+ code: 'datasource_registration_failed',
46
+ cause,
47
+ });
48
+ }
49
+ if (!response.ok) {
50
+ let detail = '';
51
+ try {
52
+ detail = (await response.text()).slice(0, 500);
53
+ }
54
+ catch {
55
+ // ignore body read failures — the status alone is enough to fail loud
56
+ }
57
+ throw new AbloError(`Direct Postgres connector registration failed (HTTP ${response.status}). ${detail}`, { code: 'datasource_registration_failed', httpStatus: response.status });
58
+ }
59
+ }
@@ -10,6 +10,7 @@
10
10
  * because the error messages reference URLs and would mislead if a
11
11
  * URL was actually present.
12
12
  */
13
+ import { AbloError } from '../errors.js';
13
14
  /**
14
15
  * Minimal subset of `AbloOptions` the validator actually inspects.
15
16
  * Defined here as its own interface so the validator doesn't pull
@@ -39,4 +40,4 @@ export interface ValidateAbloOptionsInput {
39
40
  readonly configuredApiKey: unknown;
40
41
  readonly configuredAuthToken: unknown;
41
42
  }
42
- export declare function validateAbloOptions(input: ValidateAbloOptionsInput): Error | null;
43
+ export declare function validateAbloOptions(input: ValidateAbloOptionsInput): AbloError | null;
@@ -10,12 +10,13 @@
10
10
  * because the error messages reference URLs and would mislead if a
11
11
  * URL was actually present.
12
12
  */
13
+ import { AbloValidationError } from '../errors.js';
13
14
  export function validateAbloOptions(input) {
14
15
  const { options, url, configuredApiKey, configuredAuthToken } = input;
15
16
  const kind = options.kind ?? 'user';
16
17
  if (!url) {
17
- return new Error('Ablo: `url` is required. Pass the sync server URL, e.g. ' +
18
- `Ablo({ baseURL: 'wss://sync.abloatai.com', schema, user })`);
18
+ return new AbloValidationError('Ablo: `url` is required. Pass the sync server URL, e.g. ' +
19
+ `Ablo({ baseURL: 'wss://api.abloatai.com', schema, user })`, { code: 'base_url_missing' });
19
20
  }
20
21
  // Schema is optional for the model-first API:
21
22
  // Ablo({ apiKey }).model('clauses').retrieve(...)
@@ -26,18 +27,18 @@ export function validateAbloOptions(input) {
26
27
  kind === 'user' &&
27
28
  options.user &&
28
29
  !options.user.id) {
29
- return new Error('Ablo: `user.id` must be a non-empty string when `user` is provided.');
30
+ return new AbloValidationError('Ablo: `user.id` must be a non-empty string when `user` is provided.', { code: 'invalid_options', param: 'user.id' });
30
31
  }
31
32
  if (!configuredApiKey && !configuredAuthToken && kind === 'agent' && !options.agentId) {
32
- return new Error('Ablo: provide either `apiKey` or `agentId` for `kind: "agent"`. ' +
33
+ return new AbloValidationError('Ablo: provide either `apiKey` or `agentId` for `kind: "agent"`. ' +
33
34
  'Hosted-cloud consumers pass `apiKey` and the server derives the ' +
34
35
  'agent identity from its scope; self-hosted passes `agentId` + ' +
35
- '`capabilityToken` directly.');
36
+ '`capabilityToken` directly.', { code: 'invalid_options', param: 'agentId' });
36
37
  }
37
38
  if (!configuredApiKey && !configuredAuthToken && kind === 'agent' && !options.capabilityToken) {
38
- return new Error('Ablo: provide either `apiKey` (hosted cloud — SDK exchanges internally) ' +
39
+ return new AbloValidationError('Ablo: provide either `apiKey` (hosted cloud — SDK exchanges internally) ' +
39
40
  'or `capabilityToken` (self-hosted — your auth layer mints + hands in). ' +
40
- 'See https://abloatai.com/docs/api-keys for the full pattern.');
41
+ 'See https://abloatai.com/docs/api-keys for the full pattern.', { code: 'invalid_options', param: 'capabilityToken' });
41
42
  }
42
43
  return null;
43
44
  }
@@ -8,7 +8,7 @@
8
8
  * Follows Ablo's architecture for database management.
9
9
  */
10
10
  import { getContext } from '../context.js';
11
- import { openIDBWithTimeout } from './openIDBWithTimeout.js';
11
+ import { openIDBWithTimeout, deleteIDBWithTimeout, IDBOpenTimeoutError, } from './openIDBWithTimeout.js';
12
12
  import { AbloConnectionError } from '../errors.js';
13
13
  import { getActiveRegistry, hasActiveRegistry } from '../ModelRegistry.js';
14
14
  /**
@@ -30,7 +30,7 @@ export class DatabaseManager {
30
30
  * Initialize the meta database (ablo_databases)
31
31
  */
32
32
  async initializeMetaDatabase() {
33
- this.metaDb = await openIDBWithTimeout(this.metaDbName, 1, {
33
+ const open = () => openIDBWithTimeout(this.metaDbName, 1, {
34
34
  onUpgrade: (request) => {
35
35
  const db = request.result;
36
36
  if (!db.objectStoreNames.contains('databases')) {
@@ -42,6 +42,34 @@ export class DatabaseManager {
42
42
  }
43
43
  },
44
44
  });
45
+ try {
46
+ this.metaDb = await open();
47
+ }
48
+ catch (error) {
49
+ // Self-heal a wedged meta DB. When `ablo_databases`'s backing store gets
50
+ // stuck (a corrupted store, or a leaked connection from a prior
51
+ // timed-out open), every open of that name hangs with no event and the
52
+ // app is permanently bricked until the user manually clears site data —
53
+ // the "open did not resolve within 10000ms" dead end. The registry this
54
+ // DB holds is rebuildable from the server on the next bootstrap, so it is
55
+ // safe to delete and re-create. Try exactly once: delete, then re-open.
56
+ if (!(error instanceof IDBOpenTimeoutError))
57
+ throw error;
58
+ getContext().logger.warn('[sync-engine] meta DB open timed out — attempting self-heal (delete + retry)', { db: this.metaDbName, reason: error.reason });
59
+ getContext().observability.captureBootstrapFailure(error, {
60
+ type: 'meta-db-open-timeout',
61
+ });
62
+ const deleted = await deleteIDBWithTimeout(this.metaDbName);
63
+ if (!deleted) {
64
+ // The delete itself was blocked/stuck — a live connection in another
65
+ // window or a deadlocked backing store. We cannot recover in-page;
66
+ // rethrow so the provider surfaces the real (now actionable) error.
67
+ throw error;
68
+ }
69
+ // Fresh store — this open creates `ablo_databases` from scratch.
70
+ this.metaDb = await open();
71
+ getContext().logger.info('[sync-engine] meta DB self-heal succeeded');
72
+ }
45
73
  }
46
74
  /**
47
75
  * Calculate database info for a user/workspace combination
@@ -16,12 +16,48 @@
16
16
  export declare class IDBOpenTimeoutError extends Error {
17
17
  readonly dbName: string;
18
18
  readonly reason: 'blocked' | 'timeout';
19
+ /**
20
+ * Stable, transport-independent code. `toAbloError` preserves a string
21
+ * `.code`, so this survives the wrap into `AbloError` and reaches the
22
+ * provider's `onError` intact — letting the app distinguish a wedged-storage
23
+ * failure (show a recovery screen) from any other bootstrap error without a
24
+ * brittle message match.
25
+ */
26
+ readonly code = "storage_open_timeout";
19
27
  constructor(dbName: string, reason: 'blocked' | 'timeout', message: string);
20
28
  }
29
+ /** True for the wedged-IndexedDB failure, after it has been wrapped anywhere. */
30
+ export declare function isStorageOpenTimeout(err: unknown): boolean;
21
31
  export interface OpenIDBOptions {
22
32
  /** Called inside `onupgradeneeded` — mirrors `IDBOpenDBRequest.onupgradeneeded`. */
23
33
  onUpgrade?: (request: IDBOpenDBRequest, event: IDBVersionChangeEvent) => void;
24
34
  /** Max milliseconds to wait for the open request to resolve. Default 10_000. */
25
35
  timeoutMs?: number;
36
+ /**
37
+ * Called when another context (a new tab, a fresh deploy, or our own
38
+ * `deleteIDBWithTimeout` self-heal) fires `versionchange` on this connection.
39
+ * By default the connection is `close()`d immediately — the W3C/MDN-mandated
40
+ * behavior that lets the other context's upgrade/delete proceed instead of
41
+ * blocking forever. Provide this to ALSO react (e.g. prompt a reload) AFTER
42
+ * the close. Throwing here is swallowed.
43
+ */
44
+ onVersionChange?: () => void;
26
45
  }
27
46
  export declare function openIDBWithTimeout(name: string, version: number | undefined, options?: OpenIDBOptions): Promise<IDBDatabase>;
47
+ /**
48
+ * Bounded `indexedDB.deleteDatabase()` — the delete counterpart of
49
+ * `openIDBWithTimeout`. Used by the meta-DB self-heal: when opening
50
+ * `ablo_databases` times out (a wedged backing store), we attempt to delete it
51
+ * and re-create from scratch. The registry it holds is rebuildable from the
52
+ * server on the next bootstrap, so dropping it is safe.
53
+ *
54
+ * Like `open`, `deleteDatabase` can hang indefinitely: if another live
55
+ * connection holds the DB it fires `onblocked` and waits, and on a truly stuck
56
+ * store it fires *no* event at all. Both become a bounded rejection here so the
57
+ * caller can fall through to surfacing a real error instead of spinning.
58
+ *
59
+ * Resolves `true` on a clean delete, `false` if it was blocked or timed out
60
+ * (caller decides whether to retry the open regardless — a no-op delete still
61
+ * leaves us no worse off).
62
+ */
63
+ export declare function deleteIDBWithTimeout(name: string, timeoutMs?: number): Promise<boolean>;
@@ -16,6 +16,14 @@
16
16
  export class IDBOpenTimeoutError extends Error {
17
17
  dbName;
18
18
  reason;
19
+ /**
20
+ * Stable, transport-independent code. `toAbloError` preserves a string
21
+ * `.code`, so this survives the wrap into `AbloError` and reaches the
22
+ * provider's `onError` intact — letting the app distinguish a wedged-storage
23
+ * failure (show a recovery screen) from any other bootstrap error without a
24
+ * brittle message match.
25
+ */
26
+ code = 'storage_open_timeout';
19
27
  constructor(dbName, reason, message) {
20
28
  super(message);
21
29
  this.dbName = dbName;
@@ -23,6 +31,12 @@ export class IDBOpenTimeoutError extends Error {
23
31
  this.name = 'IDBOpenTimeoutError';
24
32
  }
25
33
  }
34
+ /** True for the wedged-IndexedDB failure, after it has been wrapped anywhere. */
35
+ export function isStorageOpenTimeout(err) {
36
+ return (typeof err === 'object' &&
37
+ err !== null &&
38
+ err.code === 'storage_open_timeout');
39
+ }
26
40
  export function openIDBWithTimeout(name, version, options = {}) {
27
41
  const timeoutMs = options.timeoutMs ?? 10_000;
28
42
  return new Promise((resolve, reject) => {
@@ -42,7 +56,47 @@ export function openIDBWithTimeout(name, version, options = {}) {
42
56
  options.onUpgrade(request, event);
43
57
  };
44
58
  }
45
- request.onsuccess = () => settle(() => resolve(request.result));
59
+ request.onsuccess = () => {
60
+ // If we ALREADY timed out (or blocked) and rejected, this is a late
61
+ // success: the native open eventually completed after we gave up. The
62
+ // resulting connection is orphaned — nobody up the stack holds it, so
63
+ // nobody will `.close()` it. A leaked open connection holds an IndexedDB
64
+ // lock that wedges every subsequent open/delete of this DB name (the
65
+ // exact "ablo_databases open/delete hangs forever with no event" failure
66
+ // mode). Close it here so a timed-out attempt can't poison the store.
67
+ if (settled) {
68
+ try {
69
+ request.result.close();
70
+ }
71
+ catch {
72
+ // Best-effort — a half-open connection may already be unusable.
73
+ }
74
+ return;
75
+ }
76
+ const db = request.result;
77
+ // MANDATORY resilience handler (W3C IndexedDB / MDN): close this
78
+ // connection the instant any other context wants to upgrade or delete the
79
+ // DB. Without it, an open connection that ignores `versionchange` blocks
80
+ // the other context's request indefinitely — the root cause of a wedged
81
+ // `ablo_databases` that survives reloads (an interrupted transaction's
82
+ // connection never closes, so every later open/delete hangs with no
83
+ // event). Auto-closing here makes the store self-releasing.
84
+ db.onversionchange = () => {
85
+ try {
86
+ db.close();
87
+ }
88
+ catch {
89
+ // Already closing/closed — nothing to do.
90
+ }
91
+ try {
92
+ options.onVersionChange?.();
93
+ }
94
+ catch {
95
+ // A consumer reaction must never break the close.
96
+ }
97
+ };
98
+ settle(() => resolve(db));
99
+ };
46
100
  request.onerror = () => settle(() => reject(request.error));
47
101
  // The critical handler: another tab is blocking us. Native API leaves
48
102
  // the request pending indefinitely; we fail fast with a clear error so
@@ -61,3 +115,36 @@ export function openIDBWithTimeout(name, version, options = {}) {
61
115
  }, timeoutMs);
62
116
  });
63
117
  }
118
+ /**
119
+ * Bounded `indexedDB.deleteDatabase()` — the delete counterpart of
120
+ * `openIDBWithTimeout`. Used by the meta-DB self-heal: when opening
121
+ * `ablo_databases` times out (a wedged backing store), we attempt to delete it
122
+ * and re-create from scratch. The registry it holds is rebuildable from the
123
+ * server on the next bootstrap, so dropping it is safe.
124
+ *
125
+ * Like `open`, `deleteDatabase` can hang indefinitely: if another live
126
+ * connection holds the DB it fires `onblocked` and waits, and on a truly stuck
127
+ * store it fires *no* event at all. Both become a bounded rejection here so the
128
+ * caller can fall through to surfacing a real error instead of spinning.
129
+ *
130
+ * Resolves `true` on a clean delete, `false` if it was blocked or timed out
131
+ * (caller decides whether to retry the open regardless — a no-op delete still
132
+ * leaves us no worse off).
133
+ */
134
+ export function deleteIDBWithTimeout(name, timeoutMs = 5_000) {
135
+ return new Promise((resolve) => {
136
+ let settled = false;
137
+ const settle = (value) => {
138
+ if (settled)
139
+ return;
140
+ settled = true;
141
+ clearTimeout(timer);
142
+ resolve(value);
143
+ };
144
+ const request = indexedDB.deleteDatabase(name);
145
+ request.onsuccess = () => settle(true);
146
+ request.onerror = () => settle(false);
147
+ request.onblocked = () => settle(false);
148
+ const timer = setTimeout(() => settle(false), timeoutMs);
149
+ });
150
+ }
@@ -29,6 +29,7 @@
29
29
  * carry no `httpStatus`, exactly as Stripe omits client-side
30
30
  * programmer errors from its published code list.
31
31
  */
32
+ import { z } from 'zod';
32
33
  /**
33
34
  * Version of the error contract — the envelope shape + the set of codes and
34
35
  * their semantics. Date-based, like Stripe's API versions. Bump it (and only
@@ -36,9 +37,48 @@
36
37
  * code, a changed HTTP status, an envelope field. Emitted in `errors.json`
37
38
  * and on the `Ablo-Version` response header so a consumer can detect drift.
38
39
  */
39
- export declare const ERROR_CONTRACT_VERSION = "2026-05-28";
40
+ export declare const ERROR_CONTRACT_VERSION = "2026-06-02";
40
41
  /** Coarse grouping for metrics dashboards and docs sectioning. */
41
42
  export type ErrorCategory = 'auth' | 'permission' | 'capability' | 'claim' | 'conflict' | 'validation' | 'not_found' | 'tenant' | 'schema' | 'intent' | 'bootstrap' | 'transport' | 'rate_limit' | 'server' | 'client';
43
+ /**
44
+ * The closed taxonomy of *how a failure recovers* — one rung above the raw
45
+ * `code`. Where `code` says **what** went wrong, `RecoveryClass` says **what
46
+ * the client should do about it**, which is exactly the discriminant the sync
47
+ * FSM and the network probe need. It collapses what used to be three scattered
48
+ * booleans (`retryable`, `authBlocked`, `sessionValid`) into one exhaustive,
49
+ * Zod-validated enum so the connection layer branches on a single value with
50
+ * compile-time completeness instead of ad-hoc `if (!isRetryableCode(...))`
51
+ * chains.
52
+ *
53
+ * - `access_credential_expiry` — the Stripe-style ephemeral key (`ek_`/`rk_`)
54
+ * the sync-engine presents as its Bearer has expired. The long-lived login
55
+ * is fine; the remedy is to silently RE-MINT a fresh key from the session
56
+ * and retry the same request. This MUST NOT sign the user out (the whole
57
+ * point of the wake-from-sleep fix: a 15-min `ek_` dying after a laptop nap
58
+ * is routine, not a logout).
59
+ * - `session_expiry` — the LONG-LIVED login itself is gone. Terminal:
60
+ * sign out and route to re-authentication.
61
+ * - `auth_blocked` — reachable, but the credential TYPE/config was rejected
62
+ * (wrong key kind, untrusted issuer, no org). Re-auth re-mints the same
63
+ * rejected credential and loops, so STOP — don't reconnect, don't sign out.
64
+ * - `permission` — a 403 authorization denial (scope/role/membership).
65
+ * - `transient` — retry the same request unchanged (5xx, lease contention…).
66
+ * - `none` — not a recoverable-auth condition (validation, not-found, local
67
+ * invariants, and any forward-compat code an older SDK doesn't know).
68
+ */
69
+ export declare const RECOVERY_CLASSES: readonly ["access_credential_expiry", "session_expiry", "auth_blocked", "permission", "transient", "none"];
70
+ /** Zod enum derived from {@link RECOVERY_CLASSES} — the runtime-validatable
71
+ * form of the recovery taxonomy. */
72
+ export declare const recoveryClassSchema: z.ZodEnum<{
73
+ permission: "permission";
74
+ access_credential_expiry: "access_credential_expiry";
75
+ session_expiry: "session_expiry";
76
+ auth_blocked: "auth_blocked";
77
+ transient: "transient";
78
+ none: "none";
79
+ }>;
80
+ /** How a failure recovers. See {@link RECOVERY_CLASSES}. */
81
+ export type RecoveryClass = z.infer<typeof recoveryClassSchema>;
42
82
  /** One registry entry. `httpStatus` is present only for `surface: 'wire'`
43
83
  * codes — status is a property of the wire boundary, never of a
44
84
  * purely-local client invariant. */
@@ -55,6 +95,14 @@ export interface ErrorCodeSpec {
55
95
  readonly retryable: boolean;
56
96
  /** One-line human description — the source text for the `doc_url` page. */
57
97
  readonly message: string;
98
+ /**
99
+ * Explicit recovery class. Set ONLY where it diverges from what `category` /
100
+ * `httpStatus` / `retryable` already imply — i.e. the handful of auth codes
101
+ * whose remedy (`session_expiry` vs `access_credential_expiry`) the bare
102
+ * status can't distinguish. Everything else is derived by
103
+ * {@link classifyRecovery}, so adding a normal code needs no `recovery`.
104
+ */
105
+ readonly recovery?: RecoveryClass;
58
106
  }
59
107
  /**
60
108
  * The closed set of stable error codes. Add a code here BEFORE throwing it
@@ -69,14 +117,30 @@ export declare const ERROR_CODES: {
69
117
  readonly capability_id_missing: ErrorCodeSpec;
70
118
  readonly exchange_failed: ErrorCodeSpec;
71
119
  readonly identity_resolve_failed: ErrorCodeSpec;
120
+ readonly auth_no_credentials: ErrorCodeSpec;
121
+ readonly identity_missing_organization: ErrorCodeSpec;
72
122
  readonly session_expired: ErrorCodeSpec;
123
+ readonly jwt_invalid: ErrorCodeSpec;
124
+ readonly jwt_malformed: ErrorCodeSpec;
125
+ readonly jwt_missing_issuer: ErrorCodeSpec;
126
+ readonly jwt_issuer_untrusted: ErrorCodeSpec;
127
+ readonly jwt_signature_invalid: ErrorCodeSpec;
128
+ readonly jwt_audience_mismatch: ErrorCodeSpec;
129
+ readonly jwt_missing_subject: ErrorCodeSpec;
130
+ readonly jwt_missing_organization: ErrorCodeSpec;
131
+ readonly jwt_expired: ErrorCodeSpec;
132
+ readonly jwt_org_membership_denied: ErrorCodeSpec;
73
133
  readonly file_upload_auth_required: ErrorCodeSpec;
74
134
  readonly browser_apikey_blocked: ErrorCodeSpec;
135
+ readonly browser_database_url_blocked: ErrorCodeSpec;
136
+ readonly datasource_registration_failed: ErrorCodeSpec;
75
137
  readonly capability_scope_denied: ErrorCodeSpec;
138
+ readonly issuer_register_forbidden: ErrorCodeSpec;
76
139
  readonly capability_invalid: ErrorCodeSpec;
77
140
  readonly byo_role_cannot_enforce_rls: ErrorCodeSpec;
78
141
  readonly byo_role_unreadable: ErrorCodeSpec;
79
142
  readonly byo_tenant_tables_unforced_rls: ErrorCodeSpec;
143
+ readonly byo_host_not_allowed: ErrorCodeSpec;
80
144
  readonly claim_conflict: ErrorCodeSpec;
81
145
  readonly claim_lost: ErrorCodeSpec;
82
146
  readonly entity_claimed: ErrorCodeSpec;
@@ -92,6 +156,7 @@ export declare const ERROR_CODES: {
92
156
  readonly commit_operation_required: ErrorCodeSpec;
93
157
  readonly commit_operation_model_required: ErrorCodeSpec;
94
158
  readonly commit_operations_ambiguous: ErrorCodeSpec;
159
+ readonly commit_too_many_operations: ErrorCodeSpec;
95
160
  readonly model_required_field_missing: ErrorCodeSpec;
96
161
  readonly model_identifier_missing: ErrorCodeSpec;
97
162
  readonly snapshot_reserved_key: ErrorCodeSpec;
@@ -103,6 +168,11 @@ export declare const ERROR_CODES: {
103
168
  readonly model_not_found: ErrorCodeSpec;
104
169
  readonly mutate_update_entity_not_found: ErrorCodeSpec;
105
170
  readonly task_id_missing: ErrorCodeSpec;
171
+ readonly not_null_violation: ErrorCodeSpec;
172
+ readonly foreign_key_violation: ErrorCodeSpec;
173
+ readonly unique_violation: ErrorCodeSpec;
174
+ readonly check_violation: ErrorCodeSpec;
175
+ readonly constraint_violation: ErrorCodeSpec;
106
176
  readonly server_execute_unknown_model: ErrorCodeSpec;
107
177
  readonly mutate_create_unknown_model: ErrorCodeSpec;
108
178
  readonly tenant_model_columns_unknown: ErrorCodeSpec;
@@ -146,15 +216,18 @@ export declare const ERROR_CODES: {
146
216
  readonly queue_too_deep: ErrorCodeSpec;
147
217
  readonly flush_timeout: ErrorCodeSpec;
148
218
  readonly wait_for_timeout: ErrorCodeSpec;
219
+ readonly instance_at_capacity: ErrorCodeSpec;
149
220
  readonly fetch_unavailable: ErrorCodeSpec;
150
221
  readonly base_url_missing: ErrorCodeSpec;
151
222
  readonly sync_not_ready: ErrorCodeSpec;
152
223
  readonly ws_not_ready: ErrorCodeSpec;
153
224
  readonly quota_exceeded: ErrorCodeSpec;
225
+ readonly connection_limit_exceeded: ErrorCodeSpec;
154
226
  readonly internal_error: ErrorCodeSpec;
155
227
  readonly quota_lookup_failed: ErrorCodeSpec;
156
228
  readonly turn_open_failed: ErrorCodeSpec;
157
229
  readonly turn_close_failed: ErrorCodeSpec;
230
+ readonly invalid_options: ErrorCodeSpec;
158
231
  readonly no_ablo_provider: ErrorCodeSpec;
159
232
  readonly no_sync_group_provider: ErrorCodeSpec;
160
233
  readonly sync_context_missing_provider: ErrorCodeSpec;
@@ -189,6 +262,7 @@ export declare const ERROR_CODES: {
189
262
  readonly mutator_registry_unnamed_def: ErrorCodeSpec;
190
263
  readonly mutators_schema_missing: ErrorCodeSpec;
191
264
  readonly undo_scope_schema_missing: ErrorCodeSpec;
265
+ readonly undo_entry_invalid: ErrorCodeSpec;
192
266
  readonly mock_mutation_failed: ErrorCodeSpec;
193
267
  readonly mock_unsupported_operation: ErrorCodeSpec;
194
268
  readonly invalid_body: ErrorCodeSpec;
@@ -262,3 +336,20 @@ export declare function errorCodeSpec(code: string): ErrorCodeSpec | undefined;
262
336
  /** Whether a code's spec marks it retryable. Unknown / dynamic codes
263
337
  * default to non-retryable (safe default — don't auto-retry the unknown). */
264
338
  export declare function isRetryableCode(code: string): boolean;
339
+ /**
340
+ * Classify a `code` into its {@link RecoveryClass} — the single discriminant
341
+ * the connection FSM and the network probe branch on.
342
+ *
343
+ * The registry stays the source of truth: an explicit `spec.recovery` wins
344
+ * (set only on the few auth codes whose remedy the status can't reveal), and
345
+ * everything else is DERIVED from the spec so the registry stays terse:
346
+ * - retryable → `transient`
347
+ * - 403 → `permission`
348
+ * - residual `auth`-category → `auth_blocked` (the 401 credential-type codes)
349
+ * - otherwise / unknown → `none`
350
+ *
351
+ * Unknown / dynamic `policy:*` / forward-compat codes (`spec === undefined`)
352
+ * default to `none`, mirroring {@link isRetryableCode}'s safe default — never
353
+ * silently treat an unrecognised code as a credential expiry or a logout.
354
+ */
355
+ export declare function classifyRecovery(code: string): RecoveryClass;