@abloatai/ablo 0.9.1 → 0.9.3

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 (79) hide show
  1. package/AGENTS.md +84 -0
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +53 -27
  4. package/dist/BaseSyncedStore.d.ts +2 -36
  5. package/dist/BaseSyncedStore.js +11 -55
  6. package/dist/NetworkMonitor.js +4 -1
  7. package/dist/SyncClient.d.ts +22 -5
  8. package/dist/SyncClient.js +77 -0
  9. package/dist/SyncEngineContext.js +5 -1
  10. package/dist/agent/index.js +1 -1
  11. package/dist/api/index.d.ts +1 -1
  12. package/dist/auth/index.js +3 -1
  13. package/dist/cli.cjs +302645 -0
  14. package/dist/client/Ablo.d.ts +19 -52
  15. package/dist/client/Ablo.js +30 -106
  16. package/dist/client/ApiClient.d.ts +1 -113
  17. package/dist/client/ApiClient.js +39 -238
  18. package/dist/client/auth.js +32 -2
  19. package/dist/client/createInternalComponents.js +1 -1
  20. package/dist/client/createModelProxy.d.ts +9 -0
  21. package/dist/client/createModelProxy.js +34 -10
  22. package/dist/client/httpClient.d.ts +5 -6
  23. package/dist/client/httpClient.js +2 -3
  24. package/dist/client/index.d.ts +1 -1
  25. package/dist/client/persistence.d.ts +6 -1
  26. package/dist/client/persistence.js +1 -1
  27. package/dist/client/registerDataSource.d.ts +4 -4
  28. package/dist/client/registerDataSource.js +39 -31
  29. package/dist/client/writeOptionsSchema.d.ts +50 -0
  30. package/dist/client/writeOptionsSchema.js +57 -0
  31. package/dist/core/index.d.ts +18 -26
  32. package/dist/core/index.js +22 -46
  33. package/dist/errorCodes.d.ts +13 -0
  34. package/dist/errorCodes.js +19 -4
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.js +8 -1
  37. package/dist/interfaces/index.d.ts +14 -4
  38. package/dist/mutators/UndoManager.d.ts +48 -5
  39. package/dist/mutators/UndoManager.js +166 -1
  40. package/dist/react/AbloProvider.d.ts +18 -8
  41. package/dist/react/index.d.ts +1 -1
  42. package/dist/react/index.js +1 -1
  43. package/dist/react/useUndoScope.js +7 -0
  44. package/dist/schema/ddl.js +2 -1
  45. package/dist/schema/field.js +2 -1
  46. package/dist/schema/serialize.js +2 -1
  47. package/dist/server/commit.d.ts +4 -5
  48. package/dist/server/storage-mode.d.ts +7 -0
  49. package/dist/server/storage-mode.js +6 -0
  50. package/dist/source/adapters/drizzle.js +3 -2
  51. package/dist/source/adapters/kysely.d.ts +68 -0
  52. package/dist/source/adapters/kysely.js +210 -0
  53. package/dist/source/adapters/memory.js +2 -1
  54. package/dist/source/adapters/prisma.js +3 -2
  55. package/dist/source/index.js +2 -1
  56. package/dist/transactions/TransactionQueue.d.ts +6 -7
  57. package/dist/transactions/TransactionQueue.js +33 -9
  58. package/dist/types/streams.d.ts +2 -1
  59. package/dist/utils/duration.js +3 -2
  60. package/dist/wire/frames.d.ts +6 -8
  61. package/docs/api.md +1 -1
  62. package/docs/cli.md +17 -4
  63. package/docs/client-behavior.md +1 -1
  64. package/docs/data-sources.md +129 -125
  65. package/docs/examples/ai-sdk-tool.md +11 -5
  66. package/docs/examples/existing-python-backend.md +26 -4
  67. package/docs/examples/nextjs.md +3 -2
  68. package/docs/examples/scoped-agent.md +38 -11
  69. package/docs/guarantees.md +2 -2
  70. package/docs/identity.md +86 -59
  71. package/docs/index.md +2 -2
  72. package/docs/integration-guide.md +89 -61
  73. package/docs/mcp.md +1 -1
  74. package/docs/quickstart.md +84 -37
  75. package/docs/react.md +39 -28
  76. package/docs/schema-contract.md +2 -4
  77. package/llms-full.txt +360 -0
  78. package/llms.txt +30 -18
  79. package/package.json +23 -3
@@ -1,12 +1,13 @@
1
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.
2
+ * Self-serve direct-kind datasource registration.
6
3
  *
7
4
  * When a client is constructed with `databaseUrl`, the SDK registers that
8
5
  * connection string BEFORE bootstrap so the server resolves the org's data plane
9
- * to that direct connector.
6
+ * to that direct connection.
7
+ *
8
+ * Targets the unified `POST /v1/datasources` resource; on a 404 (an older
9
+ * server without the unified route) it falls back to the legacy
10
+ * `POST /v1/datasource` alias so an SDK upgrade never strands registration.
10
11
  *
11
12
  * The org is derived server-side from the API key — the caller never sends an
12
13
  * organization id. The connection string is sent once over TLS and is never
@@ -15,36 +16,43 @@
15
16
  */
16
17
  import { AbloError } from '../errors.js';
17
18
  /**
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.
19
+ * POST the connection string to the self-serve datasource route. Resolves on
20
+ * success (the org's data plane now points at this DB); throws an `AbloError`
21
+ * with `datasource_registration_failed` otherwise so `ready()` surfaces it
22
+ * instead of silently bootstrapping against the wrong store.
22
23
  */
23
24
  export async function registerDataSource(input) {
24
25
  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
+ throw new AbloError('databaseUrl requires an apiKey to register the database connection (the org is derived from the key).', { code: 'datasource_registration_failed' });
26
27
  }
27
28
  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
- });
29
+ const base = input.baseUrl.replace(/\/+$/, '');
30
+ const body = JSON.stringify({
31
+ connectionString: input.databaseUrl,
32
+ ...(input.schema ? { schema: input.schema } : {}),
33
+ });
34
+ const post = async (endpoint) => {
35
+ try {
36
+ return await doFetch(endpoint, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'content-type': 'application/json',
40
+ authorization: `Bearer ${input.apiKey}`,
41
+ },
42
+ body,
43
+ });
44
+ }
45
+ catch (cause) {
46
+ throw new AbloError('Could not reach the Ablo API to register the database connection.', {
47
+ code: 'datasource_registration_failed',
48
+ cause,
49
+ });
50
+ }
51
+ };
52
+ let response = await post(`${base}/v1/datasources`);
53
+ if (response.status === 404) {
54
+ // Older server without the unified resource — use the legacy alias.
55
+ response = await post(`${base}/v1/datasource`);
48
56
  }
49
57
  if (!response.ok) {
50
58
  let detail = '';
@@ -54,6 +62,6 @@ export async function registerDataSource(input) {
54
62
  catch {
55
63
  // ignore body read failures — the status alone is enough to fail loud
56
64
  }
57
- throw new AbloError(`Direct Postgres connector registration failed (HTTP ${response.status}). ${detail}`, { code: 'datasource_registration_failed', httpStatus: response.status });
65
+ throw new AbloError(`Database connection registration failed (HTTP ${response.status}). ${detail}`, { code: 'datasource_registration_failed', httpStatus: response.status });
58
66
  }
59
67
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * THE write-options schema — one Zod schema for the write dialect every
3
+ * door speaks (`ablo.<model>.create/update/delete`, `commits.create`, the
4
+ * HTTP model routes). Validated once at each public boundary so a plain-JS
5
+ * caller passing `onStale: 'rejct'` fails loudly at the call site with a
6
+ * typed `AbloValidationError`, not silently (or 400) at the server.
7
+ *
8
+ * Mirrors `source/contract.ts`: the schema is the runtime twin of the
9
+ * `MutationOptions` interface, with a compile-time drift guard at the
10
+ * bottom so the two can never silently diverge.
11
+ *
12
+ * Validation-only by design: callers keep their ORIGINAL options object.
13
+ * Zod's parse output strips unknown keys, and the `intent` slot legally
14
+ * carries live handles (`IntentLeaseHandle` / claim leases) whose
15
+ * `release`/`revoke` functions must survive — so we assert, never replace.
16
+ */
17
+ import { z } from 'zod';
18
+ export declare const onStaleModeSchema: z.ZodEnum<{
19
+ reject: "reject";
20
+ force: "force";
21
+ flag: "flag";
22
+ merge: "merge";
23
+ }>;
24
+ export declare const writeOptionsSchema: z.ZodObject<{
25
+ idempotencyKey: z.ZodOptional<z.ZodNullable<z.ZodString>>;
26
+ label: z.ZodOptional<z.ZodString>;
27
+ wait: z.ZodOptional<z.ZodEnum<{
28
+ confirmed: "confirmed";
29
+ queued: "queued";
30
+ }>>;
31
+ readAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
32
+ onStale: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
33
+ reject: "reject";
34
+ force: "force";
35
+ flag: "flag";
36
+ merge: "merge";
37
+ }>>>;
38
+ intent: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
39
+ id: z.ZodString;
40
+ }, z.core.$loose>]>>>;
41
+ causedByTaskId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
42
+ }, z.core.$strip>;
43
+ export type WriteOptionsInput = z.infer<typeof writeOptionsSchema>;
44
+ /**
45
+ * Assert a write-options bag against THE schema. Throws a typed
46
+ * `AbloValidationError` (`code: 'write_options_invalid'`, Stripe-style
47
+ * `param` pointing at the offending field) and returns nothing — the
48
+ * caller keeps its original object.
49
+ */
50
+ export declare function assertWriteOptions(value: unknown, context?: string): void;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * THE write-options schema — one Zod schema for the write dialect every
3
+ * door speaks (`ablo.<model>.create/update/delete`, `commits.create`, the
4
+ * HTTP model routes). Validated once at each public boundary so a plain-JS
5
+ * caller passing `onStale: 'rejct'` fails loudly at the call site with a
6
+ * typed `AbloValidationError`, not silently (or 400) at the server.
7
+ *
8
+ * Mirrors `source/contract.ts`: the schema is the runtime twin of the
9
+ * `MutationOptions` interface, with a compile-time drift guard at the
10
+ * bottom so the two can never silently diverge.
11
+ *
12
+ * Validation-only by design: callers keep their ORIGINAL options object.
13
+ * Zod's parse output strips unknown keys, and the `intent` slot legally
14
+ * carries live handles (`IntentLeaseHandle` / claim leases) whose
15
+ * `release`/`revoke` functions must survive — so we assert, never replace.
16
+ */
17
+ import { z } from 'zod';
18
+ import { AbloValidationError } from '../errors.js';
19
+ export const onStaleModeSchema = z.enum(['reject', 'force', 'flag', 'merge']);
20
+ export const writeOptionsSchema = z.object({
21
+ /** Server-side mutation_log cache key; `null` opts out of retry-safety. */
22
+ idempotencyKey: z.string().min(1).max(255).nullish(),
23
+ /** Human-readable audit tag, persisted to `mutation_log.label`. */
24
+ label: z.string().max(255).optional(),
25
+ /** Resolve when queued locally (default) or once the server confirms. */
26
+ wait: z.enum(['queued', 'confirmed']).optional(),
27
+ /** Stale guard: the sync watermark the caller's reasoning was based on. */
28
+ readAt: z.number().int().nonnegative().nullish(),
29
+ /** What the server does when the target moved past `readAt`. */
30
+ onStale: onStaleModeSchema.nullish(),
31
+ /** Claim/intent attribution — an id, or a live lease handle (loose: the
32
+ * handle's `release`/`revoke` functions ride along untouched). */
33
+ intent: z.union([z.string(), z.looseObject({ id: z.string() })]).nullish(),
34
+ /** Dormant wire-compat field; always `null` from current clients. */
35
+ causedByTaskId: z.string().nullish(),
36
+ });
37
+ /**
38
+ * Assert a write-options bag against THE schema. Throws a typed
39
+ * `AbloValidationError` (`code: 'write_options_invalid'`, Stripe-style
40
+ * `param` pointing at the offending field) and returns nothing — the
41
+ * caller keeps its original object.
42
+ */
43
+ export function assertWriteOptions(value, context) {
44
+ if (value == null)
45
+ return;
46
+ const result = writeOptionsSchema.safeParse(value);
47
+ if (result.success)
48
+ return;
49
+ const issue = result.error.issues[0];
50
+ const path = issue?.path.map(String).join('.') ?? '';
51
+ throw new AbloValidationError(`Invalid write options${context ? ` on \`${context}\`` : ''}${path ? ` at \`${path}\`` : ''}: ${issue?.message ?? 'failed validation'}.`, {
52
+ code: 'write_options_invalid',
53
+ ...(path ? { param: path } : {}),
54
+ });
55
+ }
56
+ const _writeOptionsContractInSync = [true, true];
57
+ void _writeOptionsContractInSync;
@@ -1,36 +1,28 @@
1
1
  /**
2
2
  * @abloatai/ablo/core — Framework extension
3
3
  *
4
- * Only imported by SyncedStore.ts and ApplicationStore.ts
5
- * the 2-3 files that extend or orchestrate the sync engine.
6
- * Regular model files and components should NOT import from here.
4
+ * Only imported by the handful of files that extend or orchestrate the
5
+ * sync engine (the app-shell store/provider stack, sync adapters, demo
6
+ * harnesses). Regular model files and components should NOT import from
7
+ * here — the consumer surface is `Ablo({ schema })` on the root.
8
+ *
9
+ * TRIMMED to what framework-level consumers actually import (verified by
10
+ * a monorepo-wide import scan). Everything else the engine defines stays
11
+ * module-private: if a new framework concern genuinely needs another
12
+ * primitive, add the export deliberately — don't re-widen the barrel.
7
13
  */
8
- export { BaseSyncedStore, type SyncStatus, type UserContext, type SyncedStoreConfig, type QueryResult, type SmartSyncOptions, type ModelConstructor, type ConcreteModelConstructor, BOOTSTRAP_CONFIG, } from '../BaseSyncedStore.js';
9
- export { SyncClient, type RehydrationStats } from '../SyncClient.js';
10
- export { Database, type BootstrapResult, type BootstrapRequirements } from '../Database.js';
14
+ export { BaseSyncedStore, type ModelConstructor, type ConcreteModelConstructor, } from '../BaseSyncedStore.js';
15
+ export { SyncClient } from '../SyncClient.js';
16
+ export { Database } from '../Database.js';
11
17
  export { ObjectPool, ModelScope } from '../ObjectPool.js';
12
18
  export { Model } from '../Model.js';
13
- export { LazyReferenceCollection, type LazyCollectionOptions } from '../LazyReferenceCollection.js';
19
+ export { LazyReferenceCollection, type LazyCollectionOptions, } from '../LazyReferenceCollection.js';
20
+ export { ModelRegistry, getActiveRegistry, } from '../ModelRegistry.js';
14
21
  export { postQuery, type PostQueryOptions } from '../query/client.js';
15
- export { probeNetwork, type ProbeResult } from '../sync/NetworkProbe.js';
16
- export { ConnectionManager, type ConnectionState, type ConnectionEvent, type ConnectionCallbacks, type ConnectionManagerOptions, } from '../sync/ConnectionManager.js';
17
- export { ModelRegistry, getActiveRegistry, type ExtendedReferenceMetadata, type BackReferenceMetadata } from '../ModelRegistry.js';
18
22
  export { computeFKDepthPriority, type InternalAbloOptions } from '../client/Ablo.js';
19
- export { TransactionQueue } from '../transactions/TransactionQueue.js';
20
- export { initSyncEngine, resetSyncEngine, isSyncEngineInitialized } from '../context.js';
21
- export { noopLogger, noopObservability, noopAnalytics, browserOnlineStatus, defaultSessionErrorDetector, emptyConfig, type SyncEngineContext, } from '../SyncEngineContext.js';
22
- export type { SyncEngineConfig, SyncLogger, SyncObservabilityProvider, SyncAnalytics, MutationExecutor, MutationDispatcher, SessionErrorDetector, OnlineStatusProvider, CommitResult, MutationOperation, BreadcrumbLevel, SyncBreadcrumbCategory, TransactionFailureDetails, BootstrapFailureDetails, WebSocketErrorDetails, RollbackDetails, SpanAttributes, } from '../interfaces/index.js';
23
- export { SyncSessionError } from '../errors.js';
24
- export { QueryProcessor } from './QueryProcessor.js';
25
- export { QueryView, type QueryViewOptions } from './QueryView.js';
26
- export { ViewRegistry } from './ViewRegistry.js';
27
- export { ObjectStore } from '../stores/ObjectStore.js';
28
- export { NetworkMonitor } from '../NetworkMonitor.js';
29
- export { SyncWebSocket, type SyncDelta, type VersionVector, type BootstrapHint, type SyncGroupChangePayload, type BootstrapDataEvent, type PresenceUpdateEvent, type SyncWebSocketOptions, } from '../sync/SyncWebSocket.js';
30
- export { BootstrapHelper, type BootstrapData, type BootstrapOptions, type BootstrapFetchResult } from '../sync/BootstrapHelper.js';
23
+ export type { SyncLogger, SyncObservabilityProvider, MutationExecutor, MutationDispatcher, SessionErrorDetector, OnlineStatusProvider, CommitResult, MutationOperation, } from '../interfaces/index.js';
24
+ export { SyncWebSocket, type SyncDelta, type SyncWebSocketOptions, } from '../sync/SyncWebSocket.js';
25
+ export { BootstrapHelper } from '../sync/BootstrapHelper.js';
31
26
  export { createIntentStream, type AttachableIntentStream, type IntentStreamConfig, } from '../sync/createIntentStream.js';
32
27
  export { awaitIntentGrant, type GrantTransport, } from '../sync/awaitIntentGrant.js';
33
- export { OfflineTransactionStore, offlineTxStore, Priority } from '../sync/OfflineTransactionStore.js';
34
- export { PropertyType, LoadStrategy, MutationOperationType } from '../types/index.js';
35
- export type { PropertyMetadata, ReferenceMetadata, ModelMetadata, SyncAction, DeltaPacket, BootstrapMetadata, DatabaseMetadata, } from '../types/index.js';
36
- export type { ModelData } from '../BaseSyncedStore.js';
28
+ export { LoadStrategy } from '../types/index.js';
@@ -1,52 +1,33 @@
1
1
  /**
2
2
  * @abloatai/ablo/core — Framework extension
3
3
  *
4
- * Only imported by SyncedStore.ts and ApplicationStore.ts
5
- * the 2-3 files that extend or orchestrate the sync engine.
6
- * Regular model files and components should NOT import from here.
4
+ * Only imported by the handful of files that extend or orchestrate the
5
+ * sync engine (the app-shell store/provider stack, sync adapters, demo
6
+ * harnesses). Regular model files and components should NOT import from
7
+ * here — the consumer surface is `Ablo({ schema })` on the root.
8
+ *
9
+ * TRIMMED to what framework-level consumers actually import (verified by
10
+ * a monorepo-wide import scan). Everything else the engine defines stays
11
+ * module-private: if a new framework concern genuinely needs another
12
+ * primitive, add the export deliberately — don't re-widen the barrel.
7
13
  */
8
- // Base store class
9
- export { BaseSyncedStore, BOOTSTRAP_CONFIG, } from '../BaseSyncedStore.js';
10
- // Core infrastructure
14
+ // Base store class + the constructor shapes subclasses reference
15
+ export { BaseSyncedStore, } from '../BaseSyncedStore.js';
16
+ // Core infrastructure classes
11
17
  export { SyncClient } from '../SyncClient.js';
12
18
  export { Database } from '../Database.js';
13
19
  export { ObjectPool, ModelScope } from '../ObjectPool.js';
14
20
  export { Model } from '../Model.js';
15
- export { LazyReferenceCollection } from '../LazyReferenceCollection.js';
16
- // Undo runtime `useUndoScope` hook from `@abloatai/ablo/react` is
17
- // the canonical access path. Type counterparts (`Ablo.Mutator.UndoScope`,
18
- // `Ablo.Mutator.UndoEntry`, `Ablo.Mutator.InverseOp`) live on the main `Ablo`
19
- // namespace. Direct class access (tests, non-React hosts) imports via
20
- // the package's internal subpath.
21
- // Lower-level network primitives — exposed here for the per-app demand
22
- // loaders. The main barrel hides these so consumer code converges on
23
- // `ablo.<model>.fetch(...)` for hydration. Loaders that haven't been
24
- // migrated yet can keep importing from `/core`.
21
+ export { LazyReferenceCollection, } from '../LazyReferenceCollection.js';
22
+ export { ModelRegistry, getActiveRegistry, } from '../ModelRegistry.js';
23
+ // Lower-level network read for per-app demand loaders that haven't
24
+ // migrated to `ablo.<model>.list(...)` yet.
25
25
  export { postQuery } from '../query/client.js';
26
- export { probeNetwork } from '../sync/NetworkProbe.js';
27
- export { ConnectionManager, } from '../sync/ConnectionManager.js';
28
- export { ModelRegistry, getActiveRegistry } from '../ModelRegistry.js';
29
26
  // FK-cycle / dependency-order helper — used by schema-aware test
30
- // fixtures and scaffolding tools to compute commit ordering. Lives
31
- // here because it traverses model relations and isn't part of the
32
- // consumer-facing API.
27
+ // fixtures and scaffolding tools to compute commit ordering.
33
28
  export { computeFKDepthPriority } from '../client/Ablo.js';
34
- export { TransactionQueue } from '../transactions/TransactionQueue.js';
35
- // ── Provider-facing DI types (was the deleted `/config` subpath) ──
36
- // Adapters that the consumer wires into `<AbloProvider>` props
37
- // (logger, observability, mutation executor, session-error
38
- // detector, etc.) implement these interfaces. Lives on `/core`
39
- // because most consumers don't need them; only apps that wrap the
40
- // provider with custom adapters reach for them.
41
- export { initSyncEngine, resetSyncEngine, isSyncEngineInitialized } from '../context.js';
42
- export { noopLogger, noopObservability, noopAnalytics, browserOnlineStatus, defaultSessionErrorDetector, emptyConfig, } from '../SyncEngineContext.js';
43
- export { SyncSessionError } from '../errors.js';
44
- export { QueryProcessor } from './QueryProcessor.js';
45
- export { QueryView } from './QueryView.js';
46
- export { ViewRegistry } from './ViewRegistry.js';
47
- export { ObjectStore } from '../stores/ObjectStore.js';
48
- export { NetworkMonitor } from '../NetworkMonitor.js';
49
- // Sync layer
29
+ // Sync layer the wire socket + delta shape, for sync adapters and the
30
+ // multi-agent demo harnesses.
50
31
  export { SyncWebSocket, } from '../sync/SyncWebSocket.js';
51
32
  export { BootstrapHelper } from '../sync/BootstrapHelper.js';
52
33
  // Intent coordination primitives (the lower-level pieces behind the
@@ -56,11 +37,6 @@ export { BootstrapHelper } from '../sync/BootstrapHelper.js';
56
37
  // orchestration and e2e harnesses — NOT on the consumer `.` root.
57
38
  export { createIntentStream, } from '../sync/createIntentStream.js';
58
39
  export { awaitIntentGrant, } from '../sync/awaitIntentGrant.js';
59
- // Offline transaction queuemoved out of the main barrel in the headless
60
- // audit cleanup (see docs/headless-audit.md §4.1 Task 23). The class
61
- // touches indexedDB + crypto.subtle and therefore cannot live on the main
62
- // headless-clean import path. Framework-level consumers (the few files
63
- // that orchestrate sync) import from /core explicitly.
64
- export { OfflineTransactionStore, offlineTxStore, Priority } from '../sync/OfflineTransactionStore.js';
65
- // Types used by framework-level code
66
- export { PropertyType, LoadStrategy, MutationOperationType } from '../types/index.js';
40
+ // Schema/model load strategy enum referenced by model registration in
41
+ // framework code.
42
+ export { LoadStrategy } from '../types/index.js';
@@ -134,9 +134,15 @@ export declare const ERROR_CODES: {
134
134
  readonly browser_apikey_blocked: ErrorCodeSpec;
135
135
  readonly browser_database_url_blocked: ErrorCodeSpec;
136
136
  readonly datasource_registration_failed: ErrorCodeSpec;
137
+ readonly datasource_connection_unsupported: ErrorCodeSpec;
137
138
  readonly capability_scope_denied: ErrorCodeSpec;
138
139
  readonly issuer_register_forbidden: ErrorCodeSpec;
139
140
  readonly capability_invalid: ErrorCodeSpec;
141
+ readonly test_database_not_registered: ErrorCodeSpec;
142
+ readonly database_role_cannot_enforce_rls: ErrorCodeSpec;
143
+ readonly database_role_unreadable: ErrorCodeSpec;
144
+ readonly database_tables_unforced_rls: ErrorCodeSpec;
145
+ readonly database_host_not_allowed: ErrorCodeSpec;
140
146
  readonly byo_role_cannot_enforce_rls: ErrorCodeSpec;
141
147
  readonly byo_role_unreadable: ErrorCodeSpec;
142
148
  readonly byo_tenant_tables_unforced_rls: ErrorCodeSpec;
@@ -152,6 +158,13 @@ export declare const ERROR_CODES: {
152
158
  readonly stale_context: ErrorCodeSpec;
153
159
  readonly idempotency_conflict: ErrorCodeSpec;
154
160
  readonly idempotency_key_too_long: ErrorCodeSpec;
161
+ readonly write_options_invalid: ErrorCodeSpec;
162
+ readonly source_operation_id_required: ErrorCodeSpec;
163
+ readonly source_adapter_misconfigured: ErrorCodeSpec;
164
+ readonly source_event_invalid: ErrorCodeSpec;
165
+ readonly duration_invalid: ErrorCodeSpec;
166
+ readonly schema_definition_invalid: ErrorCodeSpec;
167
+ readonly cli_invalid_arguments: ErrorCodeSpec;
155
168
  readonly turn_validation_failed: ErrorCodeSpec;
156
169
  readonly commit_operation_required: ErrorCodeSpec;
157
170
  readonly commit_operation_model_required: ErrorCodeSpec;
@@ -123,11 +123,19 @@ export const ERROR_CODES = {
123
123
  file_upload_auth_required: wire('auth', 401, false, 'File upload requires an authenticated session.'),
124
124
  browser_apikey_blocked: client('auth', 'Raw API keys must not be used from a browser context.'),
125
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.'),
126
+ datasource_registration_failed: client('auth', 'Failed to register the provided databaseUrl as a datasource.'),
127
+ datasource_connection_unsupported: wire('validation', 400, false, 'This deployment cannot register a direct (connection string) datasource — use the signed endpoint kind.'),
127
128
  // ── permission / capability (403) ──────────────────────────────────
128
129
  capability_scope_denied: wire('capability', 403, false, "The connection's resolved scope does not cover the attempted action."),
129
130
  issuer_register_forbidden: wire('permission', 403, false, 'Registering a trusted issuer requires a secret (sk_) API key.'),
130
131
  capability_invalid: wire('capability', 403, false, 'The capability is unknown, revoked, or expired.'),
132
+ test_database_not_registered: wire('permission', 403, false, 'Test mode requires a registered dev database for this org — run `npx ablo init`, or construct the client with `databaseUrl` using your test key.'),
133
+ database_role_cannot_enforce_rls: wire('permission', 403, false, 'The connected database role cannot enforce row-level security (superuser or BYPASSRLS).'),
134
+ database_role_unreadable: wire('permission', 403, false, 'The connected database role could not be introspected.'),
135
+ database_tables_unforced_rls: wire('permission', 403, false, 'Synced tables in the connected database do not have FORCE ROW LEVEL SECURITY applied.'),
136
+ database_host_not_allowed: wire('permission', 403, false, 'The connected database host resolves to a private, loopback, or link-local address and cannot be used.'),
137
+ // Deprecated spellings of the `database_*` codes above — still emitted by
138
+ // older servers; kept so they classify identically. Do not use in new code.
131
139
  byo_role_cannot_enforce_rls: wire('permission', 403, false, 'The direct Postgres connector role cannot enforce row-level security.'),
132
140
  byo_role_unreadable: wire('permission', 403, false, 'The direct Postgres connector role could not be introspected.'),
133
141
  byo_tenant_tables_unforced_rls: wire('permission', 403, false, 'Tenant tables do not have RLS forced under the direct Postgres connector role.'),
@@ -146,6 +154,13 @@ export const ERROR_CODES = {
146
154
  idempotency_conflict: wire('conflict', 409, false, 'The same Idempotency-Key was reused with a different request body.'),
147
155
  idempotency_key_too_long: wire('validation', 400, false, 'The supplied Idempotency-Key exceeds the maximum length.'),
148
156
  // ── validation (400 / 422) ─────────────────────────────────────────
157
+ write_options_invalid: client('validation', 'The write options (`idempotencyKey` / `label` / `wait` / `readAt` / `onStale` / `intent`) failed validation against the write-options schema.'),
158
+ source_operation_id_required: client('validation', 'A data-source operation arrived without the entity `id` it targets.'),
159
+ source_adapter_misconfigured: client('validation', 'The data-source ORM adapter could not map a schema model onto the backing client (missing delegate or model).'),
160
+ source_event_invalid: client('validation', 'A data-source outbox event could not be built — the operation carries no entity id and none was supplied.'),
161
+ duration_invalid: client('validation', 'A duration value was not a number of seconds or a "500ms" | "30s" | "3m" | "24h" string.'),
162
+ schema_definition_invalid: client('validation', 'A schema definition value was invalid (bad column identifier, non-finite backfill, or unsupported schema-JSON version).'),
163
+ cli_invalid_arguments: client('validation', 'The CLI was invoked with an unknown flag or a malformed flag value.'),
149
164
  turn_validation_failed: wire('validation', 422, false, 'The agent turn failed server-side validation.'),
150
165
  commit_operation_required: wire('validation', 400, false, 'A commit must carry `operation` or `operations`.'),
151
166
  commit_operation_model_required: wire('validation', 400, false, 'A commit operation is missing its `model`.'),
@@ -176,8 +191,8 @@ export const ERROR_CODES = {
176
191
  check_violation: wire('validation', 400, false, 'A value violates a database check constraint.'),
177
192
  constraint_violation: wire('validation', 400, false, 'A database integrity constraint was violated.'),
178
193
  // ── tenant / unknown model (400) ───────────────────────────────────
179
- server_execute_unknown_model: wire('tenant', 400, false, 'The server-execute request named a model not in the tenant schema.'),
180
- mutate_create_unknown_model: wire('tenant', 400, false, 'A create targeted a model not in the tenant schema.'),
194
+ server_execute_unknown_model: wire('tenant', 400, false, 'Wrote to a model the server does not know. The server keeps its own copy of the schema — run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` before writing to new or changed models.'),
195
+ mutate_create_unknown_model: wire('tenant', 400, false, 'Created a model the server does not know. Run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` first — the server keeps its own copy of the schema.'),
181
196
  tenant_model_columns_unknown: wire('tenant', 400, false, "The tenant model's columns could not be resolved."),
182
197
  tenant_model_missing_organization_id: wire('tenant', 400, false, 'The tenant model is missing the organization_id column required for isolation.'),
183
198
  // ── schema migration / declaration (validation) ────────────────────
@@ -286,7 +301,7 @@ export const ERROR_CODES = {
286
301
  provisioner_unavailable: wire('server', 503, false, 'No database provisioner is configured.'),
287
302
  invalid_model: wire('validation', 400, false, 'The request named an invalid model.'),
288
303
  invalid_id: wire('validation', 400, false, 'The request carried an invalid id.'),
289
- unknown_model: wire('tenant', 400, false, 'The request named a model not in the tenant schema.'),
304
+ unknown_model: wire('tenant', 400, false, 'Named a model the server does not know. Run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` — the server keeps its own copy of the schema.'),
290
305
  model_not_tenant_scoped: wire('tenant', 400, false, 'The model is not tenant-scoped and cannot be queried this way.'),
291
306
  schema_table_invalid: wire('schema', 500, false, "The model's table identifier is invalid."),
292
307
  schema_scope_invalid: wire('schema', 500, false, "The model's scope predicate could not be built."),
package/dist/index.d.ts CHANGED
@@ -64,6 +64,9 @@ export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionErr
64
64
  export type { CommitReceipt, RequiredCapability } from './errors.js';
65
65
  export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec, RecoveryClass } from './errors.js';
66
66
  export { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL } from './auth/credentialSource.js';
67
+ export { writeOptionsSchema, onStaleModeSchema, assertWriteOptions, } from './client/writeOptionsSchema.js';
68
+ export type { WriteOptionsInput } from './client/writeOptionsSchema.js';
69
+ export type { WriteOptions, MutationOptions } from './interfaces/index.js';
67
70
  export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
68
71
  export type { Register, DefaultSyncShape } from './types/global.js';
69
72
  export { defineMutators } from './mutators/defineMutators.js';
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_UR
61
61
  // Participant types live under `Ablo.Participant.*` —
62
62
  // `Ablo.Participant.Joined`, `Ablo.Participant.Manager`,
63
63
  // `Ablo.Participant.JoinOptions`, etc. Same dot-access shape as
64
- // `Ablo.Peer`, `Ablo.Claim`, `Ablo.Turn`. No flat re-exports.
64
+ // `Ablo.Peer`, `Ablo.Claim`. No flat re-exports.
65
65
  // Advanced — most apps never import this. Principal constructors for
66
66
  // delegated agent paths (`Ablo({ kind: 'agent', as: session({...}) })`).
67
67
  // The default `Ablo({ schema, apiKey })` resolves identity from the key;
@@ -89,6 +89,13 @@ export { defaultPolicy, capabilityPreemptPolicy } from './policy/index.js';
89
89
  // `e.type === 'AbloX'`) plus the HTTP-response translator.
90
90
  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';
91
91
  export { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL } from './auth/credentialSource.js';
92
+ // THE write-options contract — the one Zod schema for the option bag every
93
+ // write door accepts (`ablo.<model>.create/update/delete`, `commits.create`,
94
+ // the HTTP model routes). The SDK validates against it at each boundary;
95
+ // it's exported so consumers can validate/compose options ahead of a call
96
+ // (e.g. an agent tool's input schema). Runtime twin of `MutationOptions`,
97
+ // drift-guarded at compile time.
98
+ export { writeOptionsSchema, onStaleModeSchema, assertWriteOptions, } from './client/writeOptionsSchema.js';
92
99
  // Storage-wedge detection — lets app shells render a recovery screen when the
93
100
  // IndexedDB backing store is stuck (see core/openIDBWithTimeout.ts).
94
101
  export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
@@ -164,13 +164,23 @@ export interface MutationOptions {
164
164
  readonly id: string;
165
165
  } | null;
166
166
  /**
167
- * Active agent turn id to stamp on every delta row produced by this
168
- * commit. Forwarded as the wire-level `causedByTaskId` field on the
169
- * `{ type: 'commit' }` envelope. Set automatically by the SDK while
170
- * `beginTurn(...)` is open.
167
+ * Dormant agent-task lineage field, forwarded as the wire-level
168
+ * `causedByTaskId`. Turns/tasks were removed from the SDK; nothing
169
+ * populates this anymore (write attribution rides on the claim/intent
170
+ * id). Kept optional for wire-compat; always `null` from the client.
171
171
  */
172
172
  causedByTaskId?: string | null;
173
173
  }
174
+ /**
175
+ * The `MutationOptions` subset carried per-write through the offline
176
+ * transaction lane (SyncClient → TransactionQueue → wire operation).
177
+ * ONE shared type so the proxy's public params, the queue, and the wire
178
+ * can never narrow each other silently again — `wait` and `intent` are
179
+ * deliberately absent because they resolve client-side before staging
180
+ * (`wait` at the proxy's confirmation await, `intent` server-side via
181
+ * the active lease on the entity).
182
+ */
183
+ export type WriteOptions = Pick<MutationOptions, 'readAt' | 'onStale' | 'idempotencyKey' | 'label'>;
174
184
  /** A single mutation operation in a batch. `options` rides along so the
175
185
  * server can cache+replay via `mutation_log`. */
176
186
  export interface MutationOperation {
@@ -52,11 +52,6 @@ export interface UndoScopeOptions {
52
52
  */
53
53
  recordFromStream?: boolean;
54
54
  }
55
- /**
56
- * A single undo stack for one surface. Access via `UndoManager.getScope(name)`.
57
- * Consumers call `record(entry)` after each mutator; `undo()` / `redo()` to
58
- * traverse the stacks.
59
- */
60
55
  export declare class UndoScope<S extends Schema> {
61
56
  private readonly schema;
62
57
  private readonly store;
@@ -75,6 +70,15 @@ export declare class UndoScope<S extends Schema> {
75
70
  * observer can never wedge the editor's recording path.
76
71
  */
77
72
  private readonly recordListeners;
73
+ /**
74
+ * Observers notified after ANY stack change — record, undo, redo, or clear.
75
+ * Distinct from {@link recordListeners} (forward actions only): this fires on
76
+ * reversals too, so React consumers can keep `canUndo`/`canRedo` live. The
77
+ * stream-recording path pushes entries WITHOUT a React render, so without this
78
+ * a freshly-recorded entry leaves `canUndo` stale (snapshot from last render)
79
+ * and a Cmd+Z handler gated on `canUndo !== false` silently no-ops.
80
+ */
81
+ private readonly changeListeners;
78
82
  /**
79
83
  * Serialization tail. Recording, undo, and redo all chain off this single
80
84
  * promise so they run strictly in the order they were *invoked* — never
@@ -112,6 +116,23 @@ export declare class UndoScope<S extends Schema> {
112
116
  * response) collapses into ONE Cmd+Z. `endGroup()` flushes it.
113
117
  */
114
118
  private group;
119
+ /**
120
+ * ASYNC replay-echo suppression, keyed by `${modelKey}:${id}`.
121
+ *
122
+ * The synchronous {@link replaying} flag only catches echoes delivered INLINE
123
+ * during `applyOps`. The real engine doesn't emit `transaction:created`
124
+ * synchronously: `SyncClient` defers the commit behind `scheduleSync()` +
125
+ * `await persistMutationQueue()` (an IndexedDB write), so a replayed write's
126
+ * echo lands on the stream AFTER `undo()`/`redo()` has already reset
127
+ * `replaying` and pushed the entry. That late echo would be recorded as a
128
+ * NEW edit — and `record()` clears the redo stack, so every undo silently
129
+ * destroyed its own redo. We mark the (modelKey,id) of every op we're about
130
+ * to replay here (synchronously, before the write), and consume one mark when
131
+ * the matching mutation arrives — independent of WHEN it arrives. Entries
132
+ * carry a TTL so a never-arriving echo (offline: the commit is skipped) can't
133
+ * leak and wrongly suppress a much-later genuine edit to the same row.
134
+ */
135
+ private readonly pendingReplayEchoes;
115
136
  constructor(schema: S, store: SyncStoreContract, organizationId: string, options?: UndoScopeOptions);
116
137
  /**
117
138
  * Open a grouping session: every stream-recorded op until {@link endGroup}
@@ -122,6 +143,20 @@ export declare class UndoScope<S extends Schema> {
122
143
  beginGroup(label?: string): void;
123
144
  /** Close the grouping session and record the accumulated ops as one entry. */
124
145
  endGroup(label?: string): void;
146
+ /** Every `${modelKey}:${id}` a set of ops will touch (all op kinds). */
147
+ private replayEchoKeys;
148
+ /**
149
+ * Arm async-echo suppression for the rows a replay is about to write. Called
150
+ * synchronously, before `applyOps`, so the marks exist no matter how long the
151
+ * engine takes to surface the echo on the stream. See {@link pendingReplayEchoes}.
152
+ */
153
+ private markReplayEchoes;
154
+ /**
155
+ * If `${schemaKey}:${modelId}` has an armed echo mark, consume one and report
156
+ * that this mutation is our own replay echo (caller drops it). Prunes expired
157
+ * marks opportunistically so a skipped/never-arriving echo can't leak.
158
+ */
159
+ private consumeReplayEcho;
125
160
  /** Resolve a stream mutation's registered name to its schema key, or null. */
126
161
  private resolveSchemaKey;
127
162
  /**
@@ -175,6 +210,14 @@ export declare class UndoScope<S extends Schema> {
175
210
  */
176
211
  onRecord(listener: (entry: UndoEntry) => void): () => void;
177
212
  private emitRecord;
213
+ /**
214
+ * Subscribe to ANY stack change (record/undo/redo/clear). Used by
215
+ * `useUndoScope` to re-render so `canUndo`/`canRedo` stay live across every
216
+ * consumer — not just the component that invoked undo/redo. Returns an
217
+ * unsubscribe function.
218
+ */
219
+ onChange(listener: () => void): () => void;
220
+ private emitChange;
178
221
  canUndo(): boolean;
179
222
  canRedo(): boolean;
180
223
  /**