@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
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ERROR egress — turn ANY thrown value into Stripe's error-object envelope plus
3
+ * an HTTP status, so every error response across the Ablo surface carries the
4
+ * identical `{ type, code, param, message, doc_url, request_id }` shape
5
+ * regardless of which route or service produced it.
6
+ *
7
+ * This is the wire-PRODUCE counterpart to `errors.ts`'s wire-PARSE
8
+ * (`translateHttpError`/`errorFromWire`). It lives in `wire/` — not in the main
9
+ * SDK entry — so a server-side consumer (a Next.js route) can import it without
10
+ * dragging in the client runtime (mobx/react/IndexedDB).
11
+ *
12
+ * The classifier is the UNIVERSAL baseline: a typed {@link AbloError} passes
13
+ * through (subclass + code + httpStatus preserved), everything else degrades to
14
+ * a 500 `internal_error`. Service-specific normalization that needs a DB driver
15
+ * (apps/sync-server classifies raw Postgres SQLSTATE + MutatorError) is layered
16
+ * on top in that service — it is intentionally NOT pulled into the shared,
17
+ * dependency-free contract.
18
+ */
19
+ import { AbloError, docUrlForCode } from '../errors.js';
20
+ import { errorCodeSpec } from '../errorCodes.js';
21
+ /** {@link AbloError} subclass → default HTTP status. The subclass is chosen to
22
+ * match status semantics (a validation error is a 400, a permission error a
23
+ * 403), so a throw site only picks the right class + code and the status
24
+ * follows — an explicit `httpStatus` is passed only when it diverges (e.g. a
25
+ * 404 on the base class, a 503 on AbloServerError). Mirrors the same table in
26
+ * apps/sync-server's self-contained `errors.ts`. */
27
+ export function statusForType(type) {
28
+ switch (type) {
29
+ case 'AbloAuthenticationError':
30
+ return 401;
31
+ case 'AbloPermissionError':
32
+ return 403;
33
+ case 'AbloValidationError':
34
+ return 400;
35
+ case 'AbloRateLimitError':
36
+ return 429;
37
+ case 'AbloIdempotencyError':
38
+ case 'AbloStaleContextError':
39
+ case 'AbloClaimedError':
40
+ return 409;
41
+ case 'AbloConnectionError':
42
+ return 503;
43
+ case 'AbloServerError':
44
+ return 500;
45
+ default:
46
+ return 500;
47
+ }
48
+ }
49
+ /**
50
+ * Convert ANY thrown value into the canonical {@link ErrorEnvelope} plus an
51
+ * HTTP status. A typed {@link AbloError} is serialized via its own `toJSON`
52
+ * (so `code`/`param`/`doc_url`/structured `details` survive) and gets its
53
+ * status from an explicit `httpStatus` or, failing that, {@link statusForType}.
54
+ * Anything else degrades to a 500 `internal_error` envelope — never a bare
55
+ * framework "Internal Server Error" text body, and never a raw error string
56
+ * leaked onto the wire as an unregistered code.
57
+ *
58
+ * `requestId` is stamped into the body when the error didn't already carry one,
59
+ * so the response and the `x-request-id` header agree for support correlation.
60
+ */
61
+ export function errorEnvelope(err, requestId) {
62
+ if (err instanceof AbloError) {
63
+ // Status precedence: an explicit httpStatus wins; else the code's canonical
64
+ // status from the registry (so `new AbloError('…', { code: 'entity_not_found' })`
65
+ // is a 404 without the throw site repeating it); else the subclass default.
66
+ const status = err.httpStatus ??
67
+ (err.code ? errorCodeSpec(err.code)?.httpStatus : undefined) ??
68
+ statusForType(err.type);
69
+ const body = err.toJSON();
70
+ return {
71
+ body: requestId && body.request_id === undefined ? { ...body, request_id: requestId } : body,
72
+ status,
73
+ };
74
+ }
75
+ const message = err instanceof Error ? err.message : String(err);
76
+ return {
77
+ body: {
78
+ type: 'AbloServerError',
79
+ code: 'internal_error',
80
+ message,
81
+ doc_url: docUrlForCode('internal_error'),
82
+ ...(requestId ? { request_id: requestId } : {}),
83
+ },
84
+ status: 500,
85
+ };
86
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `@abloatai/ablo/wire` — canonical COMMIT-PATH frame contract.
3
+ *
4
+ * These are the WebSocket (and HTTP-fallback) message shapes for the
5
+ * write path: the client's `commit` / `mutation` frames and the server's
6
+ * `mutation_result` ack. They live here — not in the server app and not
7
+ * inlined in the SDK's `SyncWebSocket` — so the client, the server, and
8
+ * any future `@abloatai/ablo/server` host all import ONE definition
9
+ * and cannot drift.
10
+ *
11
+ * Scope note: the delta/sync frames (`sync_response`, `delta`) are NOT
12
+ * here yet — they reference `SyncDelta`, which currently has two
13
+ * definitions (server `db/deltas` vs package `core`) pending unification.
14
+ * They stay server-local until that lands. Everything in this file
15
+ * depends only on package-canonical types (`OnStaleMode`, `ErrorCode`,
16
+ * `RequiredCapability`), so it is safe to share today.
17
+ *
18
+ * Changing any shape here is a wire-contract change — it requires
19
+ * coordinated client + server updates.
20
+ */
21
+ import type { OnStaleMode } from '../coordination/index.js';
22
+ import type { ErrorCode, RequiredCapability } from '../errors.js';
23
+ /**
24
+ * A single operation within a {@link CommitMessage} batch. The atomic unit
25
+ * the server's commit executor applies (and, once the mutator seam lands,
26
+ * the raw-op fallback path when no named mutator is registered).
27
+ */
28
+ export interface CommitOperation {
29
+ type: 'CREATE' | 'UPDATE' | 'DELETE' | 'ARCHIVE' | 'UNARCHIVE';
30
+ model: string;
31
+ id?: string | null;
32
+ input?: Record<string, unknown> | null;
33
+ /**
34
+ * Per-op client transaction id. Stamped onto `sync_deltas.transaction_id`
35
+ * so the originating client can recognize the broadcast as an echo of its
36
+ * own optimistic mutation. Distinct from the batch-level `clientTxId`
37
+ * (which keys `mutation_log` for retry idempotency).
38
+ */
39
+ transactionId?: string | null;
40
+ /**
41
+ * Watermark from `context.capture`. The server checks whether the target
42
+ * has received deltas since this id; if so the operation's `onStale` mode
43
+ * applies.
44
+ */
45
+ readAt?: number | null;
46
+ /**
47
+ * Mode on stale detection. `'reject'` (default) throws
48
+ * AbloStaleContextError; `'force'` applies unconditionally. `'flag'` /
49
+ * `'merge'` are reserved, not yet implemented.
50
+ */
51
+ onStale?: OnStaleMode | null;
52
+ }
53
+ /**
54
+ * Client → Server single named-mutation frame. The named-mutator write
55
+ * primitive (intent + args), as opposed to the raw-op {@link CommitMessage}
56
+ * batch. Server-side mutator dispatch resolves `mutatorName` against the
57
+ * host-provided registry.
58
+ */
59
+ export interface MutationMessage {
60
+ type: 'mutation';
61
+ payload: {
62
+ mutatorName: string;
63
+ input: unknown;
64
+ clientTxId: string;
65
+ };
66
+ }
67
+ /**
68
+ * Client → Server "commit this batch of operations" frame. Formerly named
69
+ * `batch_ack` / `BatchAckMessage` — renamed pre-stable to the customer-facing
70
+ * verb (`commit`) consistently across the wire and the SDK method
71
+ * (`MutationExecutor.commit`).
72
+ */
73
+ export interface CommitMessage {
74
+ type: 'commit';
75
+ payload: {
76
+ operations: CommitOperation[];
77
+ clientTxId: string;
78
+ /**
79
+ * Optional turn handle. When the SDK opens a turn via
80
+ * `SyncAgent.beginTurn(...)`, subsequent commits within the handle's
81
+ * scope auto-attach the `turnId` here. The Hub validates the turn
82
+ * belongs to the same agent and is open, then threads it onto every
83
+ * delta's `caused_by_task_id` column. Absent for human-direct commits
84
+ * and for SDKs that predate the turn protocol — those produce deltas
85
+ * with `caused_by_task_id = NULL`, which the audit pane treats as "no
86
+ * prompt-side context recorded."
87
+ */
88
+ causedByTaskId?: string | null;
89
+ };
90
+ }
91
+ /**
92
+ * Wire ack for a `commit` frame. Payload mirrors the canonical
93
+ * `CommitReceipt` shape so WebSocket, HTTP `/v1/commits`, and persisted
94
+ * `AgentJob.result.receipt` all carry identical fields.
95
+ *
96
+ * `object`, `status`, and `ops` are typed optional because pre-unification
97
+ * WS clients didn't ship them; servers always populate them on the way out.
98
+ * New clients can rely on them.
99
+ */
100
+ export interface MutationResultMessage {
101
+ type: 'mutation_result';
102
+ payload: {
103
+ object?: 'commit_receipt';
104
+ clientTxId: string;
105
+ serverTxId: string;
106
+ success: boolean;
107
+ status?: 'confirmed' | 'rejected';
108
+ lastSyncId?: number;
109
+ ops?: number;
110
+ error?: {
111
+ code: ErrorCode;
112
+ message: string;
113
+ field?: string;
114
+ /** Structured rejection body (x402-style) emitted when the cap
115
+ * verifier denies the commit. */
116
+ requiredCapability?: RequiredCapability;
117
+ };
118
+ };
119
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `@abloatai/ablo/wire` — the canonical HTTP/frame WIRE CONTRACT, with no
3
+ * client-runtime (mobx / react / IndexedDB) dependency, so a server-side
4
+ * consumer — a Next.js route handler, an edge function — can import the
5
+ * envelope producers without pulling in the whole sync client.
6
+ *
7
+ * Two halves, both Stripe-shaped and used across every Ablo surface:
8
+ * - ERROR egress — {@link errorEnvelope} / {@link ErrorEnvelope} /
9
+ * {@link statusForType} turn any thrown value into
10
+ * `{ type, code, param, message, doc_url, request_id }`.
11
+ * - LIST egress — {@link listEnvelope} / {@link ListEnvelope} stamp the
12
+ * uniform `{ object: 'list', data, has_more, next_cursor }` collection.
13
+ *
14
+ * The {@link AbloError} hierarchy + {@link docUrlForCode} + the wire-PARSE
15
+ * helpers are re-exported so a route can THROW the right typed error and
16
+ * SERIALIZE it through a single import.
17
+ */
18
+ export { errorEnvelope, statusForType } from './errorEnvelope.js';
19
+ export type { ErrorEnvelope } from './errorEnvelope.js';
20
+ export { listEnvelope } from './listEnvelope.js';
21
+ export type { ListEnvelope } from './listEnvelope.js';
22
+ export type { CommitOperation, MutationMessage, CommitMessage, MutationResultMessage, } from './frames.js';
23
+ export { AbloError, AbloAuthenticationError, AbloPermissionError, AbloValidationError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, SyncSessionError, docUrlForCode, translateHttpError, errorFromWire, toAbloError, ERROR_CONTRACT_VERSION, } from '../errors.js';
24
+ export type { ErrorCode, WireErrorCode } from '../errors.js';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `@abloatai/ablo/wire` — the canonical HTTP/frame WIRE CONTRACT, with no
3
+ * client-runtime (mobx / react / IndexedDB) dependency, so a server-side
4
+ * consumer — a Next.js route handler, an edge function — can import the
5
+ * envelope producers without pulling in the whole sync client.
6
+ *
7
+ * Two halves, both Stripe-shaped and used across every Ablo surface:
8
+ * - ERROR egress — {@link errorEnvelope} / {@link ErrorEnvelope} /
9
+ * {@link statusForType} turn any thrown value into
10
+ * `{ type, code, param, message, doc_url, request_id }`.
11
+ * - LIST egress — {@link listEnvelope} / {@link ListEnvelope} stamp the
12
+ * uniform `{ object: 'list', data, has_more, next_cursor }` collection.
13
+ *
14
+ * The {@link AbloError} hierarchy + {@link docUrlForCode} + the wire-PARSE
15
+ * helpers are re-exported so a route can THROW the right typed error and
16
+ * SERIALIZE it through a single import.
17
+ */
18
+ export { errorEnvelope, statusForType } from './errorEnvelope.js';
19
+ export { listEnvelope } from './listEnvelope.js';
20
+ // The error surface a wire consumer needs to throw, classify, and serialize.
21
+ export { AbloError, AbloAuthenticationError, AbloPermissionError, AbloValidationError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, SyncSessionError, docUrlForCode, translateHttpError, errorFromWire, toAbloError, ERROR_CONTRACT_VERSION, } from '../errors.js';
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The canonical Ablo LIST envelope — the one shape every endpoint that returns
3
+ * a collection uses, so a consumer can detect + paginate any list uniformly
4
+ * instead of learning a per-endpoint payload key (`{ keys }`, `{ origins }`,
5
+ * `{ events }`, `{ buckets }`…).
6
+ *
7
+ * `{ object: 'list', data: [...], has_more, next_cursor }` is the shape the
8
+ * hosted `GET /v1/models/:model` endpoint already emits (apps/sync-server
9
+ * `routes/query.ts`) and that `@ablo/mcp` already consumes — promoted here so
10
+ * sync-web's dashboard lists, the SDK, and any future surface produce the
11
+ * identical envelope from one definition.
12
+ *
13
+ * The field NAMES are Stripe's (`object`/`has_more`/`next_cursor`), not
14
+ * PlanetScale's (`type`/`cursor_start`/`has_next`): the rest of the Ablo API is
15
+ * Stripe-modeled, so this keeps one vocabulary across the surface. The
16
+ * PlanetScale discipline we deliberately borrow is *"every list is the same
17
+ * envelope"* — not the concrete key names.
18
+ */
19
+ export interface ListEnvelope<T> {
20
+ /** Discriminator — always `'list'`. Lets a generic client recognise a
21
+ * paginated collection without per-endpoint special-casing. */
22
+ readonly object: 'list';
23
+ /** The page of results. Always present (an empty array when there are none),
24
+ * never omitted, so `body.data` is a stable access path. */
25
+ readonly data: readonly T[];
26
+ /** Whether more results exist past this page. Drive "load more" off this,
27
+ * not off `data.length === limit` (ambiguous on an exact-multiple page). */
28
+ readonly has_more: boolean;
29
+ /** Opaque cursor to pass back as `?starting_after=` for the next page, or
30
+ * `null` when {@link has_more} is `false`. */
31
+ readonly next_cursor: string | null;
32
+ }
33
+ /**
34
+ * Stamp the uniform {@link ListEnvelope} onto an already-resolved page of rows.
35
+ *
36
+ * Pagination stays the caller's responsibility (fetch `limit + 1`, decide
37
+ * `hasMore`, derive the cursor from the last row's order key) — this only
38
+ * applies the envelope so no endpoint hand-rolls the shape. The defaults model
39
+ * the common "small, unpaginated collection" case (`has_more: false`,
40
+ * `next_cursor: null`); a paginated endpoint passes both explicitly.
41
+ */
42
+ export declare function listEnvelope<T>(data: readonly T[], opts?: {
43
+ hasMore?: boolean;
44
+ nextCursor?: string | null;
45
+ }): ListEnvelope<T>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Stamp the uniform {@link ListEnvelope} onto an already-resolved page of rows.
3
+ *
4
+ * Pagination stays the caller's responsibility (fetch `limit + 1`, decide
5
+ * `hasMore`, derive the cursor from the last row's order key) — this only
6
+ * applies the envelope so no endpoint hand-rolls the shape. The defaults model
7
+ * the common "small, unpaginated collection" case (`has_more: false`,
8
+ * `next_cursor: null`); a paginated endpoint passes both explicitly.
9
+ */
10
+ export function listEnvelope(data, opts = {}) {
11
+ return {
12
+ object: 'list',
13
+ data,
14
+ has_more: opts.hasMore ?? false,
15
+ next_cursor: opts.nextCursor ?? null,
16
+ };
17
+ }
package/docs/api-keys.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # API Keys
2
2
 
3
- Trusted runtimes authenticate with an API key.
3
+ Authenticate a server-side client — a route handler, worker, or CLI — by passing an API key when you create the client.
4
4
 
5
5
  ```ts
6
6
  import Ablo from '@abloatai/ablo';
@@ -10,11 +10,11 @@ const ablo = Ablo({ apiKey: process.env.ABLO_API_KEY });
10
10
 
11
11
  The key identifies the Ablo account. Application code does not pass an organization id; Ablo derives scope from the credential.
12
12
 
13
- Use the root `@abloatai/ablo` import with a schema for app clients.
13
+ "Trusted" means the runtime can hold a secret: a backend or other server-side environment a browser can't read. Browser and app clients use the same `@abloatai/ablo` import but authenticate differently they never carry a secret key.
14
14
 
15
15
  ## Server-Side API Keys
16
16
 
17
- Use API keys from trusted runtimes:
17
+ Use API keys from trusted (server-side) runtimes:
18
18
 
19
19
  - backend route handlers
20
20
  - workers and agents
@@ -48,8 +48,8 @@ restricted to exactly those grants:
48
48
  high-risk, org-wide grant: because schema is shared, a push affects the live
49
49
  table shape. A full-authority key has it implicitly; a *restricted* key (such
50
50
  as a sandbox key) needs it granted explicitly.
51
- - `sandbox:<id>` — marks the key as belonging to a sandbox (its data isolation
52
- comes from the sandbox binding, not this scope string).
51
+ - `sandbox:<id>` — identifies which sandbox the key belongs to. (The key's data
52
+ isolation comes from that sandbox binding, not from this scope string.)
53
53
 
54
54
  A key minted from the default **Test mode** sandbox carries `schema:push`, so
55
55
  `ablo dev` works out of the box. Keys from other sandboxes are **data-only** by
package/docs/api.md CHANGED
@@ -1,10 +1,21 @@
1
1
  # API
2
2
 
3
- Start with the schema client:
4
-
5
- For end-to-end app setup across React, existing backends, Data Source, and
6
- agents, read [Integration Guide](./integration-guide.md).
3
+ This is the per-method reference for reading and writing rows that stay in
4
+ sync across sessions. You declare your models once, then call the same
5
+ `ablo.<model>` methods from React, a server action, or an agent — and every
6
+ confirmed write streams to everyone watching. When two writers touch the same
7
+ row, you can optionally `claim` it so they serialize instead of clobbering
8
+ each other.
9
+
10
+ Two things to know before the method list. **Reads come in two flavors:**
11
+ `retrieve(id)` / `list({ where })` are async and hit the server (use them when
12
+ the row may not be local yet); `get(id)` / `getAll({ where })` / `getCount({ where })`
13
+ are synchronous reads off the local graph (use them in render, after data has
14
+ synced). **Claims don't lock.** If another writer holds the row, `claim` waits
15
+ for them, re-reads the fresh row, then hands it to you — so two writers
16
+ serialize instead of clobbering.
7
17
 
18
+ Start with the schema client:
8
19
 
9
20
  ```ts
10
21
  import Ablo from '@abloatai/ablo';
@@ -20,39 +31,46 @@ const schema = defineSchema({
20
31
  const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
21
32
 
22
33
  await ablo.ready();
23
- const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
34
+ const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
24
35
  if (!report) throw new Error('Row not found');
25
36
 
26
- await ablo.weatherReports.update('report_stockholm', { status: 'ready' }, { wait: 'confirmed' });
37
+ await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }, wait: 'confirmed' });
27
38
  ```
28
39
 
40
+ For end-to-end app setup across React, existing backends, Data Source, and
41
+ agents, read the [Integration Guide](./integration-guide.md).
42
+
29
43
  ## Model Methods
30
44
 
31
45
  Each schema model becomes a typed model on the client:
32
46
 
33
- - `ablo.weatherReports.load({ where })` hydrates rows asynchronously.
34
- - `ablo.weatherReports.retrieve(id)` reads one already-loaded row synchronously.
35
- - `ablo.weatherReports.create(data)` creates a row.
36
- - `ablo.weatherReports.update(id, data, options?)` updates a row.
37
- - `ablo.weatherReports.delete(id, options?)` deletes a row.
47
+ - `ablo.weatherReports.retrieve({ id })` reads one row asynchronously (server read).
48
+ - `ablo.weatherReports.list({ where })` reads a collection asynchronously (server read).
49
+ - `ablo.weatherReports.get(id)` reads one row synchronously from the local graph.
50
+ - `ablo.weatherReports.create({ data })` creates a row.
51
+ - `ablo.weatherReports.update({ id, data, ...options })` updates a row.
52
+ - `ablo.weatherReports.delete({ id, ...options })` deletes a row.
38
53
 
39
- `load` and `retrieve` are not aliases. Use `load` when the row may not be loaded
40
- yet. Use `retrieve` after `ready()` or `load()` when you want a cheap
41
- synchronous read.
54
+ `retrieve`/`list` and `get`/`getAll`/`getCount` are not aliases. Use
55
+ `retrieve({ id })` or `list({ where })` when the row may not be local yet — they
56
+ hydrate pool → IndexedDB → network. Use `get(id)` / `getAll({ where })` /
57
+ `getCount({ where })` for a cheap synchronous snapshot of what is already in
58
+ the local graph.
42
59
 
43
60
  | Method | Returns | Use when |
44
61
  |---|---|---|
45
- | `load({ where })` | `Promise<T[]>` | You need to hydrate rows from local store and server. |
46
- | `retrieve(id)` | `T \| undefined` | You already loaded the row and want a synchronous read. |
47
- | `list(options?)` | `T[]` | You want a synchronous list of loaded rows. |
48
- | `count(options?)` | `number` | You want a synchronous count of loaded rows. |
49
- | `create(data, options?)` | `Promise<T>` | You want to create through the schema model. |
50
- | `update(id, data, options?)` | `Promise<T>` | You want to update through the schema model. |
51
- | `delete(id, options?)` | `Promise<void>` | You want to delete through the schema model. |
52
-
53
- `load`, `create`, `update`, and `delete` are the main path — they go through the
54
- server. `retrieve` / `list` / `count` are **synchronous reads** off the rows a
55
- session has already loaded, so a cheap re-read needs no round-trip.
62
+ | `retrieve({ id })` | `Promise<T \| undefined>` | You need one row, hydrating from local store and server. |
63
+ | `list({ where })` | `Promise<T[]>` | You need to hydrate a collection from local store and server. |
64
+ | `get(id)` | `T \| undefined` | You want a synchronous snapshot of one local row. |
65
+ | `getAll(options?)` | `T[]` | You want a synchronous snapshot of a local collection. |
66
+ | `getCount(options?)` | `number` | You want a synchronous count of local rows. |
67
+ | `create({ data, ...options })` | `Promise<T>` | You want to create through the schema model. |
68
+ | `update({ id, data, ...options })` | `Promise<T>` | You want to update through the schema model. |
69
+ | `delete({ id, ...options })` | `Promise<void>` | You want to delete through the schema model. |
70
+
71
+ `retrieve`, `list`, `create`, `update`, and `delete` are the main path they go
72
+ through the server. `get` / `getAll` / `getCount` are **synchronous reads**
73
+ off the rows a session has already synced, so a cheap re-read needs no round-trip.
56
74
 
57
75
  ## Protected Writes
58
76
 
@@ -61,11 +79,13 @@ Use `snapshot` when a write should reject if the row changed mid-flight:
61
79
  ```ts
62
80
  const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
63
81
 
64
- await ablo.weatherReports.update(
65
- 'report_stockholm',
66
- { status: 'ready' },
67
- { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
68
- );
82
+ await ablo.weatherReports.update({
83
+ id: 'report_stockholm',
84
+ data: { status: 'ready' },
85
+ readAt: snap.stamp,
86
+ onStale: 'reject',
87
+ wait: 'confirmed',
88
+ });
69
89
  ```
70
90
 
71
91
  Protected write options:
@@ -80,23 +100,26 @@ Protected write options:
80
100
 
81
101
  ## Claims
82
102
 
83
- A claim tells humans and agents who is working on a target before the write
84
- lands. One self-describing object carries the lifecycle in a single `status`
85
- field. It lives on the coordination plane: ephemeral, TTL'd, broadcast to peers
86
- in real time, and never persisted as a row.
103
+ Before anyone writes a row, they can claim it so other people and agents see
104
+ who is editing it in real time. Claims don't lock. If another writer holds the
105
+ row, `claim` waits for them, re-reads the fresh row, then hands it to you — so
106
+ two writers serialize instead of clobbering. A claim is temporary: it expires
107
+ on its own if the holder stops, and is never saved as a row.
87
108
 
88
- Coordinate one through flat verbs on the model, beside `create`/`update`/`retrieve`:
89
- `ablo.<model>.claim(id, ...)` to claim a row, `ablo.<model>.claimState(id)` to read
90
- who holds it (synchronous; never blocks), and `ablo.<model>.release(id)` to release
91
- early. Claims are **advisory** they serialize on contention rather than locking.
109
+ You coordinate a row with calls on its model, beside `create`/`update`/`retrieve`:
110
+ `ablo.<model>.claim({ id })` takes the claim and returns a handle,
111
+ `ablo.<model>.claim.state({ id })` reads who currently holds it (synchronous, never
112
+ blocks), and `ablo.<model>.claim.release({ id })` releases it early. The full
113
+ coordination surface is `claim.state({ id })` / `claim.queue({ id })` /
114
+ `claim.release({ id })` / `claim.reorder({ id, order })` hanging off `claim`.
92
115
 
93
116
  ### The Claim State Object
94
117
 
95
118
  | Field | Type | Description |
96
119
  |---|---|---|
97
- | `object` | `'claim'` | String representing the object's type. |
120
+ | `object` | `'intent'` | String representing the object's type. |
98
121
  | `id` | string | Unique identifier for the claim. |
99
- | `status` | `'active' \| 'committed' \| 'expired' \| 'canceled'` | The whole lifecycle, in one field. |
122
+ | `status` | `'active' \| 'queued' \| 'committed' \| 'expired' \| 'canceled'` | The whole lifecycle, in one field. `active` is the holder; `queued` is a waiter in the FIFO line behind it. |
100
123
  | `target` | `{ type, id, field? }` | What is being coordinated. |
101
124
  | `action` | string | Human-readable phase — `'editing'`, `'writing'`, `'reviewing'`. |
102
125
  | `heldBy` | string | Participant id holding the claim. |
@@ -105,7 +128,7 @@ early. Claims are **advisory** — they serialize on contention rather than lock
105
128
 
106
129
  ```json
107
130
  {
108
- "object": "claim",
131
+ "object": "intent",
109
132
  "id": "claim_3MtwBwLkdIwHu7ix",
110
133
  "status": "active",
111
134
  "target": { "type": "weatherReports", "id": "report_stockholm", "field": "status" },
@@ -119,7 +142,7 @@ early. Claims are **advisory** — they serialize on contention rather than lock
119
142
  ### Lifecycle
120
143
 
121
144
  ```
122
- claim(id) update(id) lands
145
+ claim({ id }) update({ id }) lands
123
146
  (free) ───────────▶ active ───────────────────────▶ committed
124
147
 
125
148
  ┌───────────┴───────────┐
@@ -128,45 +151,82 @@ early. Claims are **advisory** — they serialize on contention rather than lock
128
151
  (release w/o write) (TTL; holder died)
129
152
  ```
130
153
 
131
- A target is free when `ablo.<model>.claimState(id)` is `null`. Terminal
132
- states drop out of the live stream, so a present claim is active.
154
+ A target is free when `ablo.<model>.claim.state({ id })` is `null`. Terminal
155
+ states drop out of the live stream, so a present claim is either `active` (the
156
+ holder) or `queued` (waiting in the FIFO line behind the holder; see
157
+ `claim.queue({ id })`).
133
158
 
134
159
  ### Reading and claiming
135
160
 
136
- `claimState(id)` is the read side for observers: synchronous, never blocks, and
137
- returns the live claim state object (or `null`). `claim(id, ...)` is the write side:
138
- it claims the row and returns the row. Because the claim is **advisory**, if
139
- someone else already holds the row, `claim` waits for them to finish, then
140
- re-reads the row before handing it back so you always proceed from fresh state.
141
- Default reads stay open; server/model reads can opt into `ifClaimed: 'wait'` or
142
- `ifClaimed: 'fail'` when they should not read through active work.
161
+ `claim.state({ id })` is the read side for observers: synchronous, never blocks, and
162
+ returns the live claim state object (or `null`). `claim({ id })` is the write
163
+ side: it takes the claim and returns a `ClaimHandle`. Claims don't lock if someone else
164
+ already holds the row, `claim` waits for them to finish, re-reads the fresh row,
165
+ then hands it to you, so you always proceed from current state. Default reads
166
+ return the row even while someone is mid-edit; if a server read should not
167
+ return a row while it's claimed, pass `ifClaimed: 'wait'` to wait for the claim
168
+ to clear, or `ifClaimed: 'fail'` to error out instead.
143
169
 
144
170
  ```ts
145
- const claim = ablo.weatherReports.claimState('report_stockholm');
171
+ const claim = ablo.weatherReports.claim.state({ id: 'report_stockholm' });
146
172
  if (claim) {
147
173
  claim.heldBy;
148
174
  claim.action;
149
175
  }
150
176
 
151
- const updated = await ablo.weatherReports.claim(
152
- 'report_stockholm',
153
- async (report) => ablo.weatherReports.update(report.id, { status: 'ready' }),
154
- { action: 'editing', ttl: '2m' },
155
- );
177
+ const handle = await ablo.weatherReports.claim({
178
+ id: 'report_stockholm',
179
+ action: 'editing',
180
+ ttl: '2m',
181
+ });
182
+ await ablo.weatherReports.update({ id: handle.data.id, data: { status: 'ready' } });
183
+ await handle.release();
156
184
  ```
157
185
 
158
- Writes go through the normal flat `ablo.<model>.update(id, data)`. While you hold
159
- a claim on `id`, that `update` is automatically stale-guarded: it rejects with
160
- `AbloStaleContextError` if the row advanced past your claim point, so you re-read
161
- before retrying. The callback form releases automatically when the callback
162
- returns or throws, or call `ablo.weatherReports.release(id)` if you claimed manually and
163
- need to release early.
186
+ Writes go through the normal `ablo.<model>.update({ id, data })`. While you hold
187
+ a claim on `id`, that `update` rejects with `AbloStaleContextError` if the row
188
+ changed underneath you since you took the claim, so you re-read before retrying.
189
+ Call `handle.release()` (or `ablo.weatherReports.claim.release({ id })`) to release
190
+ the claim when your work is done.
164
191
 
165
192
  ## Agent
166
193
 
167
194
  Most agents should import the same schema as the app and call
168
- `ablo.<model>.load(...)`, `ablo.<model>.claim(...)`, and
169
- `ablo.<model>.update(...)`.
195
+ `ablo.<model>.list(...)`, `ablo.<model>.claim({ id })`, and
196
+ `ablo.<model>.update({ id, data })`.
197
+
198
+ ## HTTP API
199
+
200
+ The SDK is a convenience wrapper over a model-scoped HTTP surface — the same
201
+ noun (`model`) and verbs as `ablo.<model>.…`. Non-JS callers (or curl) use it
202
+ directly. The table below shows the shape with `{model}` as a placeholder; the
203
+ [OpenAPI spec](./openapi.json) expands it into one **typed** path per model
204
+ (`/v1/models/task`, `/v1/models/deck`, …, generated from your schema) so each
205
+ endpoint documents that model's real field contract instead of a generic blob.
206
+
207
+ | SDK call | HTTP |
208
+ |---|---|
209
+ | `ablo.<model>.create({ data })` | `POST /v1/models/{model}` |
210
+ | `ablo.<model>.list({ where })` | `GET /v1/models/{model}` |
211
+ | `ablo.<model>.retrieve({ id })` | `GET /v1/models/{model}/{id}` |
212
+ | `ablo.<model>.update({ id, data })` | `PATCH /v1/models/{model}/{id}` |
213
+ | `ablo.<model>.delete({ id })` | `DELETE /v1/models/{model}/{id}` |
214
+ | `ablo.<model>.claim({ id })` | `POST /v1/models/{model}/{id}/claim` |
215
+ | (release a claim) | `DELETE /v1/models/{model}/{id}/claim` |
216
+
217
+ Auth is a bearer API key: `Authorization: Bearer sk_…`. Mutations take an
218
+ `Idempotency-Key` header — derive it from the business event, not a random
219
+ value, so a retry never double-writes. Writes return a `CommitReceipt`; a
220
+ rejected write carries an error `code` (e.g. `stale_context`, `intent_conflict`)
221
+ to act on. `GET /v1/models/{model}` is cursor-paginated (`limit`, `order`,
222
+ `order_by`, `starting_after`) and returns `{ data, has_more, next_cursor }`.
223
+
224
+ `POST /v1/commits` remains the path for **atomic multi-op** writes (several
225
+ operations across rows/models that must commit together) — the per-model routes
226
+ above are the one-record path. Both run the identical guarded-write engine.
227
+
228
+ The [coordination MCP server](./mcp.md) (`@ablo/mcp`) is this same surface
229
+ rendered as agent tools.
170
230
 
171
231
  ## Errors
172
232