@abloatai/ablo 0.3.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 (278) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +12 -0
  4. package/README.md +230 -0
  5. package/dist/BaseSyncedStore.d.ts +709 -0
  6. package/dist/BaseSyncedStore.js +1843 -0
  7. package/dist/Database.d.ts +344 -0
  8. package/dist/Database.js +1259 -0
  9. package/dist/LazyReferenceCollection.d.ts +181 -0
  10. package/dist/LazyReferenceCollection.js +460 -0
  11. package/dist/Model.d.ts +339 -0
  12. package/dist/Model.js +715 -0
  13. package/dist/ModelRegistry.d.ts +200 -0
  14. package/dist/ModelRegistry.js +535 -0
  15. package/dist/NetworkMonitor.d.ts +27 -0
  16. package/dist/NetworkMonitor.js +73 -0
  17. package/dist/ObjectPool.d.ts +202 -0
  18. package/dist/ObjectPool.js +1106 -0
  19. package/dist/SyncClient.d.ts +489 -0
  20. package/dist/SyncClient.js +1555 -0
  21. package/dist/SyncEngineContext.d.ts +46 -0
  22. package/dist/SyncEngineContext.js +74 -0
  23. package/dist/adapters/alwaysOnline.d.ts +16 -0
  24. package/dist/adapters/alwaysOnline.js +19 -0
  25. package/dist/adapters/inMemoryStorage.d.ts +30 -0
  26. package/dist/adapters/inMemoryStorage.js +94 -0
  27. package/dist/agent/Agent.d.ts +358 -0
  28. package/dist/agent/Agent.js +500 -0
  29. package/dist/agent/index.d.ts +115 -0
  30. package/dist/agent/index.js +128 -0
  31. package/dist/agent/session.d.ts +90 -0
  32. package/dist/agent/session.js +156 -0
  33. package/dist/agent/types.d.ts +73 -0
  34. package/dist/agent/types.js +10 -0
  35. package/dist/ai-sdk/coordination-context.d.ts +51 -0
  36. package/dist/ai-sdk/coordination-context.js +107 -0
  37. package/dist/ai-sdk/index.d.ts +68 -0
  38. package/dist/ai-sdk/index.js +68 -0
  39. package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
  40. package/dist/ai-sdk/intent-broadcast.js +72 -0
  41. package/dist/ai-sdk/wrap.d.ts +67 -0
  42. package/dist/ai-sdk/wrap.js +45 -0
  43. package/dist/api/index.d.ts +10 -0
  44. package/dist/api/index.js +9 -0
  45. package/dist/auth/index.d.ts +137 -0
  46. package/dist/auth/index.js +246 -0
  47. package/dist/client/Ablo.d.ts +835 -0
  48. package/dist/client/Ablo.js +1440 -0
  49. package/dist/client/ApiClient.d.ts +200 -0
  50. package/dist/client/ApiClient.js +659 -0
  51. package/dist/client/auth.d.ts +79 -0
  52. package/dist/client/auth.js +81 -0
  53. package/dist/client/createInternalComponents.d.ts +44 -0
  54. package/dist/client/createInternalComponents.js +88 -0
  55. package/dist/client/createModelProxy.d.ts +152 -0
  56. package/dist/client/createModelProxy.js +199 -0
  57. package/dist/client/identity.d.ts +63 -0
  58. package/dist/client/identity.js +156 -0
  59. package/dist/client/index.d.ts +36 -0
  60. package/dist/client/index.js +33 -0
  61. package/dist/client/persistence.d.ts +7 -0
  62. package/dist/client/persistence.js +11 -0
  63. package/dist/client/validateAbloOptions.d.ts +42 -0
  64. package/dist/client/validateAbloOptions.js +43 -0
  65. package/dist/config/index.d.ts +10 -0
  66. package/dist/config/index.js +12 -0
  67. package/dist/context.d.ts +27 -0
  68. package/dist/context.js +58 -0
  69. package/dist/core/DatabaseManager.d.ts +108 -0
  70. package/dist/core/DatabaseManager.js +361 -0
  71. package/dist/core/QueryProcessor.d.ts +77 -0
  72. package/dist/core/QueryProcessor.js +262 -0
  73. package/dist/core/QueryView.d.ts +64 -0
  74. package/dist/core/QueryView.js +219 -0
  75. package/dist/core/StoreManager.d.ts +131 -0
  76. package/dist/core/StoreManager.js +334 -0
  77. package/dist/core/ViewRegistry.d.ts +20 -0
  78. package/dist/core/ViewRegistry.js +55 -0
  79. package/dist/core/index.d.ts +34 -0
  80. package/dist/core/index.js +59 -0
  81. package/dist/core/openIDBWithTimeout.d.ts +27 -0
  82. package/dist/core/openIDBWithTimeout.js +63 -0
  83. package/dist/core/query-utils.d.ts +37 -0
  84. package/dist/core/query-utils.js +60 -0
  85. package/dist/errors.d.ts +235 -0
  86. package/dist/errors.js +243 -0
  87. package/dist/index.d.ts +41 -0
  88. package/dist/index.js +82 -0
  89. package/dist/interfaces/headless.d.ts +95 -0
  90. package/dist/interfaces/headless.js +41 -0
  91. package/dist/interfaces/index.d.ts +321 -0
  92. package/dist/interfaces/index.js +8 -0
  93. package/dist/mutators/RecordingTransaction.d.ts +36 -0
  94. package/dist/mutators/RecordingTransaction.js +216 -0
  95. package/dist/mutators/Transaction.d.ts +48 -0
  96. package/dist/mutators/Transaction.js +64 -0
  97. package/dist/mutators/UndoManager.d.ts +114 -0
  98. package/dist/mutators/UndoManager.js +143 -0
  99. package/dist/mutators/defineMutators.d.ts +55 -0
  100. package/dist/mutators/defineMutators.js +28 -0
  101. package/dist/policy/index.d.ts +19 -0
  102. package/dist/policy/index.js +18 -0
  103. package/dist/policy/types.d.ts +74 -0
  104. package/dist/policy/types.js +17 -0
  105. package/dist/principal.d.ts +44 -0
  106. package/dist/principal.js +49 -0
  107. package/dist/query/client.d.ts +43 -0
  108. package/dist/query/client.js +84 -0
  109. package/dist/query/index.d.ts +6 -0
  110. package/dist/query/index.js +5 -0
  111. package/dist/query/types.d.ts +143 -0
  112. package/dist/query/types.js +36 -0
  113. package/dist/react/AbloProvider.d.ts +205 -0
  114. package/dist/react/AbloProvider.js +398 -0
  115. package/dist/react/ClientSideSuspense.d.ts +36 -0
  116. package/dist/react/ClientSideSuspense.js +17 -0
  117. package/dist/react/DefaultFallback.d.ts +24 -0
  118. package/dist/react/DefaultFallback.js +43 -0
  119. package/dist/react/SyncGroupProvider.d.ts +19 -0
  120. package/dist/react/SyncGroupProvider.js +44 -0
  121. package/dist/react/context.d.ts +161 -0
  122. package/dist/react/context.js +35 -0
  123. package/dist/react/index.d.ts +64 -0
  124. package/dist/react/index.js +73 -0
  125. package/dist/react/internalContext.d.ts +35 -0
  126. package/dist/react/internalContext.js +3 -0
  127. package/dist/react/useAblo.d.ts +72 -0
  128. package/dist/react/useAblo.js +63 -0
  129. package/dist/react/useCurrentUserId.d.ts +21 -0
  130. package/dist/react/useCurrentUserId.js +33 -0
  131. package/dist/react/useErrorListener.d.ts +20 -0
  132. package/dist/react/useErrorListener.js +39 -0
  133. package/dist/react/useIntent.d.ts +29 -0
  134. package/dist/react/useIntent.js +42 -0
  135. package/dist/react/useMutate.d.ts +83 -0
  136. package/dist/react/useMutate.js +122 -0
  137. package/dist/react/useMutationFailureListener.d.ts +26 -0
  138. package/dist/react/useMutationFailureListener.js +38 -0
  139. package/dist/react/useMutators.d.ts +56 -0
  140. package/dist/react/useMutators.js +66 -0
  141. package/dist/react/usePresence.d.ts +32 -0
  142. package/dist/react/usePresence.js +41 -0
  143. package/dist/react/useQuery.d.ts +123 -0
  144. package/dist/react/useQuery.js +145 -0
  145. package/dist/react/useReactive.d.ts +35 -0
  146. package/dist/react/useReactive.js +111 -0
  147. package/dist/react/useReader.d.ts +69 -0
  148. package/dist/react/useReader.js +73 -0
  149. package/dist/react/useSyncStatus.d.ts +61 -0
  150. package/dist/react/useSyncStatus.js +76 -0
  151. package/dist/react/useUndoScope.d.ts +36 -0
  152. package/dist/react/useUndoScope.js +73 -0
  153. package/dist/realtime/index.d.ts +10 -0
  154. package/dist/realtime/index.js +9 -0
  155. package/dist/schema/field.d.ts +134 -0
  156. package/dist/schema/field.js +264 -0
  157. package/dist/schema/index.d.ts +29 -0
  158. package/dist/schema/index.js +38 -0
  159. package/dist/schema/model.d.ts +326 -0
  160. package/dist/schema/model.js +89 -0
  161. package/dist/schema/queries.d.ts +203 -0
  162. package/dist/schema/queries.js +145 -0
  163. package/dist/schema/relation.d.ts +172 -0
  164. package/dist/schema/relation.js +104 -0
  165. package/dist/schema/schema.d.ts +259 -0
  166. package/dist/schema/schema.js +188 -0
  167. package/dist/schema/sugar.d.ts +129 -0
  168. package/dist/schema/sugar.js +94 -0
  169. package/dist/source/index.d.ts +423 -0
  170. package/dist/source/index.js +320 -0
  171. package/dist/source/pushQueue.d.ts +112 -0
  172. package/dist/source/pushQueue.js +249 -0
  173. package/dist/stores/ObjectStore.d.ts +103 -0
  174. package/dist/stores/ObjectStore.js +371 -0
  175. package/dist/stores/ObjectStoreContract.d.ts +39 -0
  176. package/dist/stores/ObjectStoreContract.js +1 -0
  177. package/dist/stores/SyncActionStore.d.ts +101 -0
  178. package/dist/stores/SyncActionStore.js +481 -0
  179. package/dist/sync/BootstrapHelper.d.ts +127 -0
  180. package/dist/sync/BootstrapHelper.js +434 -0
  181. package/dist/sync/ConnectionManager.d.ts +136 -0
  182. package/dist/sync/ConnectionManager.js +465 -0
  183. package/dist/sync/HydrationCoordinator.d.ts +137 -0
  184. package/dist/sync/HydrationCoordinator.js +468 -0
  185. package/dist/sync/NetworkProbe.d.ts +43 -0
  186. package/dist/sync/NetworkProbe.js +113 -0
  187. package/dist/sync/OfflineFlush.d.ts +9 -0
  188. package/dist/sync/OfflineFlush.js +22 -0
  189. package/dist/sync/OfflineTransactionStore.d.ts +37 -0
  190. package/dist/sync/OfflineTransactionStore.js +263 -0
  191. package/dist/sync/SyncWebSocket.d.ts +663 -0
  192. package/dist/sync/SyncWebSocket.js +1336 -0
  193. package/dist/sync/createIntentStream.d.ts +33 -0
  194. package/dist/sync/createIntentStream.js +243 -0
  195. package/dist/sync/createPresenceStream.d.ts +46 -0
  196. package/dist/sync/createPresenceStream.js +192 -0
  197. package/dist/sync/createSnapshot.d.ts +33 -0
  198. package/dist/sync/createSnapshot.js +124 -0
  199. package/dist/sync/participants.d.ts +114 -0
  200. package/dist/sync/participants.js +336 -0
  201. package/dist/sync/schemas.d.ts +79 -0
  202. package/dist/sync/schemas.js +78 -0
  203. package/dist/testing/fixtures/bootstrap.d.ts +45 -0
  204. package/dist/testing/fixtures/bootstrap.js +53 -0
  205. package/dist/testing/fixtures/deltas.d.ts +86 -0
  206. package/dist/testing/fixtures/deltas.js +139 -0
  207. package/dist/testing/fixtures/models.d.ts +82 -0
  208. package/dist/testing/fixtures/models.js +270 -0
  209. package/dist/testing/helpers/react-wrapper.d.ts +66 -0
  210. package/dist/testing/helpers/react-wrapper.js +64 -0
  211. package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
  212. package/dist/testing/helpers/sync-engine-harness.js +70 -0
  213. package/dist/testing/helpers/wait.d.ts +25 -0
  214. package/dist/testing/helpers/wait.js +44 -0
  215. package/dist/testing/index.d.ts +21 -0
  216. package/dist/testing/index.js +32 -0
  217. package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
  218. package/dist/testing/mocks/MockMutationExecutor.js +139 -0
  219. package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
  220. package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
  221. package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
  222. package/dist/testing/mocks/MockSyncContext.js +100 -0
  223. package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
  224. package/dist/testing/mocks/MockSyncStore.js +171 -0
  225. package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
  226. package/dist/testing/mocks/MockWebSocket.js +117 -0
  227. package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
  228. package/dist/transactions/OptimisticEchoTracker.js +104 -0
  229. package/dist/transactions/TransactionQueue.d.ts +499 -0
  230. package/dist/transactions/TransactionQueue.js +1895 -0
  231. package/dist/transactions/index.d.ts +16 -0
  232. package/dist/transactions/index.js +7 -0
  233. package/dist/transactions/mutation-error-handler.d.ts +5 -0
  234. package/dist/transactions/mutation-error-handler.js +39 -0
  235. package/dist/types/global.d.ts +107 -0
  236. package/dist/types/global.js +38 -0
  237. package/dist/types/index.d.ts +241 -0
  238. package/dist/types/index.js +70 -0
  239. package/dist/types/streams.d.ts +495 -0
  240. package/dist/types/streams.js +11 -0
  241. package/dist/utils/asyncIterator.d.ts +41 -0
  242. package/dist/utils/asyncIterator.js +142 -0
  243. package/dist/utils/duration.d.ts +28 -0
  244. package/dist/utils/duration.js +47 -0
  245. package/dist/utils/mobx-setup.d.ts +42 -0
  246. package/dist/utils/mobx-setup.js +381 -0
  247. package/docs/api-keys.md +24 -0
  248. package/docs/api.md +230 -0
  249. package/docs/audit.md +81 -0
  250. package/docs/capabilities.md +163 -0
  251. package/docs/client-behavior.md +202 -0
  252. package/docs/data-sources.md +214 -0
  253. package/docs/examples/agent-human.md +84 -0
  254. package/docs/examples/ai-sdk-tool.md +92 -0
  255. package/docs/examples/existing-python-backend.md +249 -0
  256. package/docs/examples/nextjs.md +88 -0
  257. package/docs/examples/server-agent.md +86 -0
  258. package/docs/guarantees.md +148 -0
  259. package/docs/index.md +97 -0
  260. package/docs/integration-guide.md +493 -0
  261. package/docs/interaction-model.md +140 -0
  262. package/docs/mcp/claude-code.md +43 -0
  263. package/docs/mcp/cursor.md +53 -0
  264. package/docs/mcp/windsurf.md +46 -0
  265. package/docs/mcp.md +59 -0
  266. package/docs/quickstart.md +152 -0
  267. package/docs/react.md +115 -0
  268. package/docs/roadmap.md +45 -0
  269. package/examples/README.md +54 -0
  270. package/examples/data-source/README.md +102 -0
  271. package/examples/data-source/ablo-driver.ts +89 -0
  272. package/examples/data-source/customer-server.ts +208 -0
  273. package/examples/data-source/run.ts +101 -0
  274. package/examples/data-source/schema.ts +25 -0
  275. package/examples/quickstart.ts +54 -0
  276. package/examples/tsconfig.json +16 -0
  277. package/llms.txt +143 -0
  278. package/package.json +147 -0
package/docs/api.md ADDED
@@ -0,0 +1,230 @@
1
+ # API
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).
7
+
8
+
9
+ ```ts
10
+ import Ablo from '@ablo/sync-engine';
11
+ import { defineSchema, model, z } from '@ablo/sync-engine/schema';
12
+
13
+ const schema = defineSchema({
14
+ tasks: model({
15
+ title: z.string(),
16
+ status: z.enum(['todo', 'doing', 'done']),
17
+ }),
18
+ });
19
+
20
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
21
+
22
+ await ablo.ready();
23
+ const [task] = await ablo.tasks.load({ where: { id: 'task_123' } });
24
+ if (!task) throw new Error('Task not found');
25
+
26
+ await ablo.tasks.update('task_123', { status: 'done' }, { wait: 'confirmed' });
27
+ ```
28
+
29
+ ## Model Methods
30
+
31
+ Each schema model becomes a typed resource on the client:
32
+
33
+ - `ablo.tasks.load({ where })` hydrates rows asynchronously.
34
+ - `ablo.tasks.retrieve(id)` reads one already-loaded row synchronously.
35
+ - `ablo.tasks.create(data)` creates a row.
36
+ - `ablo.tasks.update(id, data, options?)` updates a row.
37
+ - `ablo.tasks.delete(id, options?)` deletes a row.
38
+
39
+ `load` and `retrieve` are not aliases. Use `load` when the row may not be in the
40
+ local pool yet. Use `retrieve` after `ready()` or `load()` when you want a cheap
41
+ local read.
42
+
43
+ | Method | Returns | Use when |
44
+ |---|---|---|
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 local read. |
47
+ | `list(options?)` | `T[]` | You want a synchronous local list. |
48
+ | `count(options?)` | `number` | You want a synchronous local count. |
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
+ `list` and `count` read the local pool. They default to live rows and accept:
54
+
55
+ ```ts
56
+ const activeDoneTasks = ablo.tasks.list({
57
+ where: { status: 'done' },
58
+ filter: (task) => !task.title.startsWith('[archived]'),
59
+ orderBy: { updatedAt: 'desc' },
60
+ limit: 20,
61
+ scope: 'live', // 'live' | 'archived' | 'all'
62
+ });
63
+ ```
64
+
65
+ ## Protected Writes
66
+
67
+ Use `snapshot` when a write should reject if the row changed mid-flight:
68
+
69
+ ```ts
70
+ const snap = ablo.snapshot({ tasks: 'task_123' });
71
+
72
+ await ablo.tasks.update(
73
+ 'task_123',
74
+ { status: 'done' },
75
+ { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
76
+ );
77
+ ```
78
+
79
+ Protected write options:
80
+
81
+ | Option | Purpose |
82
+ |---|---|
83
+ | `readAt` | The state cursor the write was based on. |
84
+ | `onStale` | Stale-state policy. Prefer `reject` for agent writes. |
85
+ | `intent` | Active work claim associated with the write. |
86
+ | `wait` | `queued` resolves after local queueing; `confirmed` waits for server acceptance. |
87
+ | `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
88
+ | `timeout` | Maximum time to wait for the write call. |
89
+
90
+ ## Advanced Resource API
91
+
92
+ Use `resource(name)` only when you intentionally need the raw protocol shape:
93
+ generic server runtimes, MCP routes, batch tools, or code that has no schema.
94
+
95
+ ```ts
96
+ const tasks = ablo.resource<{ status: string }>('tasks');
97
+
98
+ const { data, stamp, intents } = await tasks.retrieve('task_123', {
99
+ ifBusy: 'return',
100
+ });
101
+ ```
102
+
103
+ `stamp` is the state watermark. Pass it as `readAt` to reject stale writes.
104
+
105
+ `intents` lists active work on the target. Set `ifBusy: 'wait'` to wait for
106
+ that work to clear, or `ifBusy: 'fail'` to throw `AbloBusyError`.
107
+
108
+ ## Intent
109
+
110
+ Intent is the coordination signal. It tells humans and agents who is working on
111
+ a target before the write lands.
112
+
113
+ ```ts
114
+ const intent = await ablo.intents.create({
115
+ target: { resource: 'tasks', id: 'task_123', field: 'status' },
116
+ action: 'update',
117
+ });
118
+
119
+ try {
120
+ const snap = ablo.snapshot({ tasks: 'task_123' });
121
+ const task = await ablo.tasks.update(
122
+ 'task_123',
123
+ { status: 'done' },
124
+ { intent, readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
125
+ );
126
+
127
+ task.status; // done
128
+ } finally {
129
+ await intent.release();
130
+ }
131
+ ```
132
+
133
+ `intents.waitFor(target)` waits until the live intent stream clears. Pass
134
+ `timeout` only when your product needs an upper bound.
135
+
136
+ ## Advanced Commit API
137
+
138
+ Most callers use `ablo.<model>.update(...)` or `resource.update(...)`. For atomic
139
+ batches or custom runtimes, use `commits.create`.
140
+
141
+ ```ts
142
+ await ablo.commits.create({
143
+ wait: 'confirmed',
144
+ operations: [
145
+ {
146
+ action: 'update',
147
+ resource: 'tasks',
148
+ id: 'task_123',
149
+ data: { status: 'done' },
150
+ readAt: stamp,
151
+ onStale: 'reject',
152
+ },
153
+ ],
154
+ });
155
+ ```
156
+
157
+ Every state change becomes a commit. Commits check authorization, stale state,
158
+ intent conflicts, and idempotency.
159
+
160
+ ## Agent
161
+
162
+ Most agents should import the same schema as the app and call
163
+ `ablo.<model>.load(...)` plus `ablo.<model>.update(...)`. The schema-less
164
+ `agent.run(...)` wrapper exists for advanced server workers that cannot import
165
+ the app schema; it creates the capability and task internally and returns
166
+ `done`, `failed`, or `cancelled`.
167
+
168
+ ## Data Source
169
+
170
+ Use `dataSource(...)` only when the customer's app database remains canonical
171
+ and Ablo should call a signed endpoint instead of storing customer rows itself.
172
+
173
+ ```ts
174
+ import { dataSource } from '@ablo/sync-engine';
175
+ import { schema } from './ablo.schema';
176
+
177
+ export const POST = dataSource({
178
+ schema,
179
+ signingSecret: process.env.ABLO_DATA_SOURCE_SIGNING_SECRET,
180
+ async commit({ operations, clientTxId, context }) {
181
+ // Write operations to the customer's database transaction.
182
+ return { rows: [] };
183
+ },
184
+ });
185
+ ```
186
+
187
+ The SDK still uses `ablo.<model>.update(...)`. The Data Source endpoint is a
188
+ server-to-server storage adapter. See [Connect Your Database](./data-sources.md).
189
+
190
+ ## Capability
191
+
192
+ Capabilities are the lower-level permission boundary. Most apps should let
193
+ `agent.run(...)` create and revoke them.
194
+
195
+ ```ts
196
+ const capability = await api.capabilities.create({
197
+ participantKind: 'agent',
198
+ participantId: 'agent:task-writer',
199
+ // Strings derive from the schema's `identityRoles` templates
200
+ // (see integration-guide.md §1) or a model's `syncGroupFormat`.
201
+ syncGroups: ['org:acme', 'user:agent:task-writer'],
202
+ operations: ['tasks.retrieve', 'tasks.update'],
203
+ lease: '10m',
204
+ });
205
+ ```
206
+
207
+ Use `lease` as a crash cleanup window. Normal agent runs still close when the
208
+ handler returns, fails, or is cancelled.
209
+
210
+ For the design rationale — why capabilities instead of static API keys,
211
+ why the lease + signature + revocation triple, and how this maps to AWS
212
+ STS / Vault / Auth0 Token Vault — see [Capabilities](./capabilities.md#why-capabilities-not-api-keys).
213
+
214
+ ## Errors
215
+
216
+ All SDK errors extend `AbloError` and expose a stable `type` string.
217
+
218
+ | Error | Meaning |
219
+ |---|---|
220
+ | `AbloAuthenticationError` | Missing, invalid, or expired credential. |
221
+ | `AbloPermissionError` | Credential is valid but the action is outside scope. |
222
+ | `AbloRateLimitError` | Rate limit or quota exceeded. |
223
+ | `AbloIdempotencyError` | Idempotency key was reused with a different request. |
224
+ | `AbloConnectionError` | Network, timeout, abort, or transport failure. |
225
+ | `AbloValidationError` | Invalid input. |
226
+ | `AbloServerError` | Server-side 5xx. |
227
+ | `AbloStaleContextError` | `readAt` no longer matches current state. |
228
+ | `AbloBusyError` | Active intent conflict or busy wait timeout. |
229
+
230
+ See [Client Behavior](./client-behavior.md) for retry and timeout guidance.
package/docs/audit.md ADDED
@@ -0,0 +1,81 @@
1
+ # Audit log
2
+
3
+ Every commit becomes one row. Rows are hash-chained per principal —
4
+ tamper-evident, queryable, exportable.
5
+
6
+ ## Row shape
7
+
8
+ ```ts
9
+ {
10
+ occurredAt: '2026-05-14T14:22:01.034Z',
11
+ actorKind: 'user' | 'agent' | 'system',
12
+ actorId: string,
13
+ onBehalfOfKind: 'user' | 'agent' | 'system' | null,
14
+ onBehalfOfId: string | null,
15
+ capabilityId: string | null,
16
+ capabilityLabel: string | null,
17
+ delegationChainRoot: string | null, // always points at a human
18
+ causedByTaskId: string | null,
19
+ actionType: string, // e.g. 'task.update'
20
+ modelName: string | null, // e.g. 'claude-opus-4-7'
21
+ diffSummary: unknown,
22
+ // tamper-evident
23
+ chainSeq: number,
24
+ prevHash: string,
25
+ rowHash: string,
26
+ }
27
+ ```
28
+
29
+ ## Delegation chain
30
+
31
+ `delegationChainRoot` always points at the human who started the chain.
32
+ There is no audit row whose root is an agent. Autonomous AI writes are not
33
+ a thing in this system; every chain starts with a person.
34
+
35
+ ## Verify
36
+
37
+ ```bash
38
+ curl https://<your-app>/api/orgs/<slug>/audit/verify-chain?\
39
+ principalKind=agent\
40
+ &principalId=task-writer-v3
41
+ ```
42
+
43
+ Returns either:
44
+
45
+ ```json
46
+ { "ok": true, "rowsChecked": 10472, "fromSeq": 1, "lastSeq": 10472 }
47
+ ```
48
+
49
+ or, on tamper:
50
+
51
+ ```json
52
+ { "ok": false, "brokenAtSeq": 8419, "reason": "hash_mismatch",
53
+ "expectedHash": "sha256:…", "foundHash": "sha256:…" }
54
+ ```
55
+
56
+ ## Filter and paginate
57
+
58
+ The dashboard at `/[orgSlug]/audit` is the UI for this. The same filters
59
+ are available on the API:
60
+
61
+ ```
62
+ GET /api/orgs/<slug>/audit?actorKind=agent&since=2026-05-01&limit=100
63
+ ```
64
+
65
+ Cursor-paginated. Continue with the `nextCursor` value from the response.
66
+
67
+ ## Export
68
+
69
+ ```bash
70
+ curl 'https://<your-app>/api/orgs/<slug>/audit/export?actorKind=agent&since=2026-05-01' \
71
+ > may-agent-writes.csv
72
+ ```
73
+
74
+ CSV up to a hard cap per request. For larger windows, paginate.
75
+
76
+ ## Compliance posture
77
+
78
+ The [audit log landing page](/audit-log) is the marketing-side description.
79
+ The verifier's hash algorithm and chain semantics live in the
80
+ `@ablo/audit-chain` package — embeddable if you need to verify chains in a
81
+ detached service.
@@ -0,0 +1,163 @@
1
+ # Capabilities
2
+
3
+ A capability is scoped credentials for a non-human actor.
4
+
5
+ It is not a task and it is not an intent. It is the permission boundary that
6
+ answers who may touch which resources.
7
+
8
+ Most apps should use `api.agent(...).run(...)`; the SDK creates and revokes the
9
+ capability for that run. Create capabilities directly only for custom runtimes,
10
+ MCP sessions, or protocol-level integrations.
11
+
12
+ ## Why capabilities, not API keys
13
+
14
+ Static API keys protect a human-operated workflow with one shared secret —
15
+ fine for a server-to-server integration written once and forgotten. They are
16
+ the wrong primitive for AI agents:
17
+
18
+ - An agent that holds an account-wide key inherits every permission your
19
+ human team has. A leaked key burns the whole account; a confused-deputy
20
+ bug lets the agent write to resources it had no business touching.
21
+ - Per-task work needs per-task attribution. One static key across every
22
+ agent invocation makes the audit trail say "the API key did it" — which
23
+ tells you nothing about which run, which prompt, or which user delegated
24
+ the action.
25
+ - Long-lived secrets accumulate blast radius. The longer a credential is
26
+ valid, the more places it travels (logs, env files, agent prompts), and
27
+ the wider the leak surface.
28
+
29
+ Capabilities replace the one-static-key model with **per-run, per-scope,
30
+ short-lived** credentials. The 2025-2026 AI-agent auth consensus (the
31
+ OAuth 2.1 / MCP spec, GCP short-lived credentials, AWS STS AssumeRole,
32
+ HashiCorp Vault leases, Auth0 Token Vault) converged on the same shape:
33
+ issue scoped tokens, attach a TTL, verify per-request, support fast
34
+ revocation. Capabilities are Ablo's instance of that pattern.
35
+
36
+ ## The three-layer security model
37
+
38
+ Every commit is authorized by three independent checks. None of them is
39
+ sufficient on its own; together they cap the blast radius of every
40
+ credible failure mode.
41
+
42
+ 1. **Lease (TTL)** — every capability has an expiry encoded in the bearer
43
+ token itself. After the lease, the token decodes but every signature
44
+ check fails. Caps the damage from a leaked token without requiring a
45
+ database lookup on the hot path.
46
+ 2. **Signature verification (per request)** — every commit re-verifies
47
+ the token's signature and attenuation. Stateless, cheap (microseconds),
48
+ detects forged or tampered tokens. The token's `syncGroups` and
49
+ `operations` are checked against the commit's actual targets;
50
+ `capability_scope_denied` rejects the request before any write lands.
51
+ 3. **Revocation** — `DELETE /v1/capabilities/:id` flips the cap's status
52
+ server-side; live WebSocket sessions are closed, future requests are
53
+ rejected within seconds. Closes the gap between lease refresh cycles
54
+ when you need *immediate* cutoff (compromised agent, accidental
55
+ over-grant, end-of-trial cleanup).
56
+
57
+ The mental model: **lease prevents the slow leak, signature verification
58
+ prevents the forged token, revocation prevents the active attacker.**
59
+ Removing any one of the three leaves a class of failure uncovered.
60
+
61
+ ## Why this shape, in one paragraph each
62
+
63
+ **Lease, not "session"** — A session token requires a database round-trip
64
+ on every request to check liveness. A lease is encoded in the token and
65
+ verified stateless. Vault popularized the term ("lease, renew, revoke");
66
+ the mechanic is the same as AWS STS time-bounded credentials and GCP
67
+ short-lived service-account creds. Ablo uses the word "lease" because the
68
+ bearer holds a *bounded grant*, not just a timer — the same word
69
+ `capability_scope_denied` errors reference.
70
+
71
+ **Two scope axes (`syncGroups` + `operations`), not one** — `syncGroups`
72
+ narrows *which rows* the actor can see; `operations` narrows *which verbs*
73
+ the actor can use. Collapsing them into one set forces an explosion
74
+ (`tasks.update:org:acme`, `tasks.delete:org:acme`, ...). Keeping them
75
+ orthogonal lets a 3-group × 5-op cap stay 3+5 instead of 3×5. Same shape
76
+ as IAM policies (`Resource` + `Action`), Stripe Restricted Keys
77
+ (`resource_type` + `permission`), and Biscuit caveats.
78
+
79
+ **Strings from `identityRoles`, never invented** — A consumer who types
80
+ `'org:acme'` literally couples their code to Ablo's identity convention.
81
+ Templates declared once on the schema (see integration-guide.md §1) let
82
+ the convention live in one place; consumers reference it by template, not
83
+ by hand-typing the prefix. Same boundary as Liveblocks' `prepareSession()`
84
+ or PowerSync's named streams: server owns the namespace, client picks a
85
+ subset by id.
86
+
87
+ **`participantKind` cannot be `'user'`** — Capabilities are explicitly
88
+ for non-human actors. A capability minted as a user would let any code
89
+ path with that bearer impersonate the human; instead, user actions flow
90
+ through session auth (cookies / OAuth) so the audit chain says
91
+ "alice@example.com did X" — not "a token did X." Stripe makes the same
92
+ split between Restricted API Keys (system) and Connect OAuth (user-on-
93
+ behalf-of).
94
+
95
+ ## What capabilities aren't
96
+
97
+ | Not | Why we didn't ship that |
98
+ |---|---|
99
+ | **A static API key** | One leaked secret = whole-account compromise. No per-run attribution. No automatic expiry. |
100
+ | **An OAuth session token** | OAuth's user-delegation model assumes a human in the loop; agents are the actor, not the delegate. The auth flow round-trips don't fit agent runtimes. |
101
+ | **An opaque DB session** | Per-request DB lookup is the slow path. Stateless verification (signature + lease) is the fast path; the DB is the revocation list, not the live-check. |
102
+ | **A bearer JWT with `exp`** | Conceptually similar, but Biscuit caveats let us *attenuate* a cap further (delegate a narrower sub-scope to a sub-agent) without re-minting. Plain JWTs can't subset themselves. |
103
+
104
+ ## Create
105
+
106
+ ```ts
107
+ import Ablo from '@ablo/sync-engine';
108
+
109
+ const admin = Ablo({ apiKey: process.env.ABLO_API_KEY });
110
+
111
+ const capability = await admin.capabilities.create({
112
+ participantKind: 'agent',
113
+ participantId: 'agent:task-writer',
114
+ // Identity-anchored groups derived from the schema's `identityRoles`
115
+ // registration (see integration-guide.md §1). The strings here mirror
116
+ // whatever templates the schema declared — `org:{id}` and friends for
117
+ // Ablo's stock schema; a third-party schema with `region:{id}` /
118
+ // `customer:{id}` roles would pass those instead.
119
+ syncGroups: ['org:acme', 'user:agent:task-writer'],
120
+ operations: ['tasks.retrieve', 'tasks.update'],
121
+ lease: '10m',
122
+ });
123
+ ```
124
+
125
+ Pass `capability.token` into the agent runtime. The agent never sees admin
126
+ credentials.
127
+
128
+ ```ts
129
+ const agent = capability.client();
130
+ ```
131
+
132
+ ## Inspect
133
+
134
+ ```ts
135
+ const record = await admin.capabilities.retrieve(capability.id);
136
+
137
+ record.status; // active | expired | revoked
138
+ record.operations; // ['tasks.retrieve', 'tasks.update']
139
+ ```
140
+
141
+ Inspection never returns the bearer token. Tokens are returned once at create
142
+ time.
143
+
144
+ ## Revoke
145
+
146
+ ```ts
147
+ await admin.capabilities.revoke(capability.id);
148
+ ```
149
+
150
+ Revocation is forward-only. Already accepted commits stand; future requests with
151
+ that token are rejected within seconds.
152
+
153
+ ## Scope Grammar
154
+
155
+ | Field | Required | Meaning |
156
+ |---|---|---|
157
+ | `participantKind` | yes | `agent` or `system`. Capabilities cannot impersonate `user`. |
158
+ | `participantId` | recommended | Stable actor id, for example `agent:task-writer`. |
159
+ | `syncGroups` | yes | Sync groups the actor can touch. Strings come from the schema's `identityRoles` templates or a model's `syncGroupFormat` — never invented by the caller. |
160
+ | `operations` | yes | Typed operation names, for example `tasks.update`. |
161
+ | `lease` / `leaseSeconds` | recommended | Crash cleanup window for abandoned actors. |
162
+ | `label` | no | Human-readable label for dashboards and audit. |
163
+ | `userMeta` | no | Customer-attested end-user metadata for B2B2C flows. |
@@ -0,0 +1,202 @@
1
+ # Client Behavior
2
+
3
+ This page covers the SDK behavior around options, errors, retries, and runtimes.
4
+
5
+ ## Constructor
6
+
7
+ ```ts
8
+ import Ablo from '@ablo/sync-engine';
9
+ import { defineSchema, model, z } from '@ablo/sync-engine/schema';
10
+
11
+ const schema = defineSchema({
12
+ tasks: model({
13
+ title: z.string(),
14
+ status: z.enum(['todo', 'doing', 'done']),
15
+ }),
16
+ });
17
+
18
+ const ablo = Ablo({
19
+ schema,
20
+ apiKey: process.env.ABLO_API_KEY,
21
+ });
22
+ ```
23
+
24
+ Common options:
25
+
26
+ | Option | Purpose |
27
+ |---|---|
28
+ | `schema` | Required for typed model resources. Omit only for advanced schema-less runtimes. |
29
+ | `apiKey` | Bearer credential for trusted server runtimes. Defaults to `ABLO_API_KEY` when available. |
30
+ | `baseURL` | Override the hosted sync endpoint for staging or private deployments. |
31
+ | `persistence` | `volatile` by default. Use `indexeddb` for browser durable cache and offline queueing. |
32
+ | `fetch` | Custom fetch implementation for tests or non-standard runtimes. |
33
+ | `defaultHeaders` | Extra headers attached to every HTTP request. |
34
+ | `defaultQuery` | Extra query parameters attached to every HTTP request. |
35
+ | `dangerouslyAllowBrowser` | Required before sending an API key from browser code. Prefer a server route instead. |
36
+
37
+ There is intentionally no `databaseURL` constructor option. Teams that keep
38
+ canonical rows in their own database use a signed [Data Source](./data-sources.md)
39
+ endpoint.
40
+
41
+ ## Model Methods
42
+
43
+ Each schema model becomes a typed resource:
44
+
45
+ ```ts
46
+ await ablo.ready();
47
+
48
+ const [task] = await ablo.tasks.load({ where: { id: 'task_123' } });
49
+ const local = ablo.tasks.retrieve('task_123');
50
+
51
+ await ablo.tasks.create({ title: 'Draft launch plan', status: 'todo' });
52
+ await ablo.tasks.update('task_123', { status: 'done' }, { wait: 'confirmed' });
53
+ await ablo.tasks.delete('task_123', { wait: 'confirmed' });
54
+ ```
55
+
56
+ `load` is async hydration from local store and server. `retrieve`, `list`, and
57
+ `count` are synchronous local reads after data is loaded.
58
+
59
+ `list` accepts the same practical read options the React selector path uses:
60
+ `where`, `filter`, `orderBy`, `limit`, `offset`, and `scope`. Scope defaults to
61
+ `'live'`; pass `'archived'` or `'all'` when you intentionally want non-live
62
+ rows.
63
+
64
+ ## Multiplayer Behavior
65
+
66
+ Multiplayer works when every participant uses the same model resource path. A
67
+ human Server Action, a browser view, and an agent worker can all use
68
+ `ablo.tasks`:
69
+
70
+ ```ts
71
+ const [task] = await ablo.tasks.load({ where: { id } });
72
+ const snap = ablo.snapshot({ tasks: id });
73
+
74
+ await ablo.tasks.update(id, patch, {
75
+ readAt: snap.stamp,
76
+ onStale: 'reject',
77
+ wait: 'confirmed',
78
+ });
79
+ ```
80
+
81
+ The confirmed write fans out over realtime subscriptions. React clients that use
82
+ `useAblo((ablo) => ablo.tasks.retrieve(id))` receive the new row, and selectors
83
+ such as `useAblo((ablo) => ablo.intents.list({ resource: 'tasks', id }))`
84
+ receive active intents. There is
85
+ no extra multiplayer setup beyond routing shared state through Ablo.
86
+
87
+ If an app writes directly to its database, Ablo cannot coordinate that write
88
+ until the app reports it through Data Source events.
89
+
90
+ ## Per-Write Options
91
+
92
+ ```ts
93
+ await ablo.tasks.update(
94
+ 'task_123',
95
+ { status: 'done' },
96
+ {
97
+ wait: 'confirmed',
98
+ readAt: snap.stamp,
99
+ onStale: 'reject',
100
+ intent,
101
+ idempotencyKey: 'task_123:mark-done:v1',
102
+ timeout: 20_000,
103
+ },
104
+ );
105
+ ```
106
+
107
+ | Option | Purpose |
108
+ |---|---|
109
+ | `wait` | `queued` resolves after local queueing; `confirmed` waits for server acceptance. |
110
+ | `readAt` | State cursor the write was based on. |
111
+ | `onStale` | Policy when the target changed after `readAt`. Prefer `reject`. |
112
+ | `intent` | Active work claim associated with this write. |
113
+ | `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
114
+ | `timeout` | Maximum time for the write call. |
115
+
116
+ ## Busy Behavior
117
+
118
+ ```ts
119
+ const busy = ablo.intents.list({ resource: 'tasks', id: 'task_123' });
120
+
121
+ if (busy.length > 0) {
122
+ await ablo.intents.waitFor(
123
+ { resource: 'tasks', id: 'task_123' },
124
+ { timeout: 30_000 },
125
+ );
126
+ }
127
+ ```
128
+
129
+ Reads never silently block. For raw resource calls, use `ifBusy`:
130
+
131
+ - `return` returns active intents.
132
+ - `wait` waits for matching intents to clear.
133
+ - `fail` throws `AbloBusyError`.
134
+
135
+ Schema clients use the realtime stream for waits. Schema-less HTTP clients must
136
+ provide `busyPollInterval` when using `ifBusy: 'wait'`.
137
+
138
+ ## Errors
139
+
140
+ All SDK errors extend `AbloError` and carry a stable `type`.
141
+
142
+ | Error | Typical cause |
143
+ |---|---|
144
+ | `AbloAuthenticationError` | Missing, invalid, or expired credential. |
145
+ | `AbloPermissionError` | Valid credential, denied operation or scope. |
146
+ | `AbloRateLimitError` | Rate limit or quota exceeded. Check `retryAfterSeconds`. |
147
+ | `AbloIdempotencyError` | Same idempotency key reused with a different request. |
148
+ | `AbloConnectionError` | Network, timeout, abort, or transport failure. |
149
+ | `AbloValidationError` | Invalid input or unsupported request shape. |
150
+ | `AbloServerError` | Server-side 5xx. Retry with backoff if the operation is idempotent. |
151
+ | `AbloStaleContextError` | Write was based on stale `readAt` state. Re-read and retry. |
152
+ | `AbloBusyError` | Active intent conflicted with `ifBusy: 'fail'` or a busy wait timed out. |
153
+
154
+ ```ts
155
+ import { AbloBusyError } from '@ablo/sync-engine';
156
+
157
+ try {
158
+ await ablo.tasks.update('task_123', { status: 'done' }, { wait: 'confirmed' });
159
+ } catch (error) {
160
+ if (error instanceof AbloBusyError) {
161
+ return { status: 'busy', intents: error.intents };
162
+ }
163
+ throw error;
164
+ }
165
+ ```
166
+
167
+ ## Retries and Idempotency
168
+
169
+ Model writes are retry-safe by default because the SDK attaches an idempotency
170
+ key. If you provide your own key, keep it stable for retries of the same logical
171
+ operation and never reuse it for a different payload.
172
+
173
+ Retry transport failures and 5xx with backoff. Do not blindly retry validation,
174
+ permission, idempotency, or stale-context errors without changing the request.
175
+
176
+ ## Logging
177
+
178
+ Pass a logger when you need SDK logs in your own observability pipeline:
179
+
180
+ ```ts
181
+ const ablo = Ablo({
182
+ schema,
183
+ apiKey: process.env.ABLO_API_KEY,
184
+ logger,
185
+ });
186
+ ```
187
+
188
+ The logger receives lifecycle, sync, retry, and rollback events. Avoid logging
189
+ request bodies that may contain customer data.
190
+
191
+ ## Public Imports
192
+
193
+ Only these imports are public SemVer surface:
194
+
195
+ - `@ablo/sync-engine`
196
+ - `@ablo/sync-engine/schema`
197
+ - `@ablo/sync-engine/react`
198
+ - `@ablo/sync-engine/testing`
199
+
200
+ `dataSource(...)` is exported from the root package for customer-owned storage
201
+ adapters. Everything outside the four import paths is internal to Ablo-owned
202
+ apps and infrastructure.