@abloatai/ablo 0.35.0 → 0.36.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 (215) hide show
  1. package/CHANGELOG.md +713 -629
  2. package/README.md +56 -519
  3. package/bin/ablo.cjs +39 -0
  4. package/dist/BaseSyncedStore.d.ts +24 -4
  5. package/dist/BaseSyncedStore.js +53 -37
  6. package/dist/Database.d.ts +8 -20
  7. package/dist/Database.js +61 -59
  8. package/dist/InstanceCache.d.ts +6 -2
  9. package/dist/InstanceCache.js +18 -16
  10. package/dist/Model.d.ts +17 -7
  11. package/dist/Model.js +17 -7
  12. package/dist/ModelRegistry.d.ts +4 -0
  13. package/dist/ModelRegistry.js +17 -15
  14. package/dist/NetworkMonitor.d.ts +3 -1
  15. package/dist/NetworkMonitor.js +7 -5
  16. package/dist/SyncClient.d.ts +5 -15
  17. package/dist/SyncClient.js +60 -57
  18. package/dist/client/Ablo.js +30 -19
  19. package/dist/client/createInternalComponents.d.ts +4 -0
  20. package/dist/client/createInternalComponents.js +8 -2
  21. package/dist/client/createModelProxy.d.ts +21 -1
  22. package/dist/client/createModelProxy.js +123 -57
  23. package/dist/client/humans.d.ts +30 -9
  24. package/dist/client/humans.js +45 -19
  25. package/dist/client/reactiveEngine.d.ts +16 -11
  26. package/dist/client/reactiveEngine.js +113 -335
  27. package/dist/client/storeCluster.d.ts +47 -0
  28. package/dist/client/storeCluster.js +118 -0
  29. package/dist/client/storeLifecycle.d.ts +61 -0
  30. package/dist/client/storeLifecycle.js +231 -0
  31. package/dist/context.d.ts +13 -0
  32. package/dist/context.js +23 -0
  33. package/dist/core/index.d.ts +1 -1
  34. package/dist/docs/catalog.js +6 -3
  35. package/dist/index.d.ts +4 -2
  36. package/dist/index.js +4 -2
  37. package/dist/query/client.d.ts +3 -0
  38. package/dist/query/client.js +6 -5
  39. package/dist/react/AbloProvider.d.ts +13 -1
  40. package/dist/react/AbloProvider.js +5 -2
  41. package/dist/react/context.d.ts +2 -2
  42. package/dist/react/createAbloReact.d.ts +56 -0
  43. package/dist/react/createAbloReact.js +51 -0
  44. package/dist/react/index.d.ts +1 -0
  45. package/dist/react/index.js +3 -0
  46. package/dist/react/useAblo.d.ts +9 -2
  47. package/dist/react/useAblo.js +25 -7
  48. package/dist/schema/coordination.js +5 -1
  49. package/dist/schema/index.d.ts +1 -0
  50. package/dist/schema/index.js +4 -0
  51. package/dist/schema/select.js +3 -0
  52. package/dist/schema/serialize.js +3 -0
  53. package/dist/source/adapter.d.ts +7 -5
  54. package/dist/source/adapter.js +7 -5
  55. package/dist/source/index.d.ts +1 -1
  56. package/dist/source/index.js +1 -1
  57. package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
  58. package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
  59. package/dist/{core → stores}/DatabaseManager.js +13 -12
  60. package/dist/{core → stores}/StoreManager.d.ts +5 -3
  61. package/dist/{core → stores}/StoreManager.js +24 -22
  62. package/dist/stores/SyncActionStore.d.ts +3 -1
  63. package/dist/stores/SyncActionStore.js +9 -7
  64. package/dist/sync/BootstrapFetcher.d.ts +4 -0
  65. package/dist/sync/BootstrapFetcher.js +28 -26
  66. package/dist/sync/OnDemandLoader.d.ts +3 -0
  67. package/dist/sync/OnDemandLoader.js +1 -0
  68. package/dist/sync/bootstrapApply.d.ts +3 -0
  69. package/dist/sync/bootstrapApply.js +2 -2
  70. package/dist/sync/deltaPipeline.d.ts +13 -12
  71. package/dist/sync/deltaPipeline.js +21 -4
  72. package/dist/sync/groupChange.d.ts +3 -0
  73. package/dist/sync/groupChange.js +16 -14
  74. package/dist/sync/participants.d.ts +19 -2
  75. package/dist/sync/participants.js +3 -1
  76. package/dist/sync/schemas.d.ts +2 -1
  77. package/dist/sync/schemas.js +3 -3
  78. package/dist/syncLog/contract.d.ts +20 -0
  79. package/dist/syncLog/contract.js +19 -0
  80. package/dist/syncLog/index.d.ts +1 -0
  81. package/dist/syncLog/index.js +1 -0
  82. package/dist/transaction/auth/capability.d.ts +35 -0
  83. package/dist/transaction/auth/capability.js +25 -0
  84. package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
  85. package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
  86. package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
  87. package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
  88. package/dist/transaction/coordination/index.d.ts +4 -4
  89. package/dist/transaction/coordination/index.js +4 -3
  90. package/dist/transaction/coordination/locator.d.ts +23 -2
  91. package/dist/transaction/coordination/locator.js +22 -2
  92. package/dist/transaction/coordination/schema.d.ts +125 -62
  93. package/dist/transaction/coordination/schema.js +228 -64
  94. package/dist/transaction/coordination/targetConflict.js +32 -28
  95. package/dist/transaction/errorCodes.d.ts +2 -2
  96. package/dist/transaction/errorCodes.js +13 -9
  97. package/dist/transaction/plugin.d.ts +95 -2
  98. package/dist/transaction/plugin.js +21 -2
  99. package/dist/transaction/resources/httpResources.d.ts +57 -2
  100. package/dist/transaction/resources/modelOperations.d.ts +148 -40
  101. package/dist/transaction/resources/where.d.ts +16 -0
  102. package/dist/transaction/resources/where.js +45 -0
  103. package/dist/transaction/schema/field.d.ts +12 -18
  104. package/dist/transaction/schema/fieldRef.d.ts +38 -0
  105. package/dist/transaction/schema/fieldRef.js +11 -0
  106. package/dist/transaction/schema/openapi.d.ts +16 -15
  107. package/dist/transaction/schema/openapi.js +186 -25
  108. package/dist/transaction/schema/relation.d.ts +7 -2
  109. package/dist/transaction/schema/schema.d.ts +27 -0
  110. package/dist/transaction/schema/schema.js +20 -0
  111. package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
  112. package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
  113. package/dist/transaction/transport/httpClient.d.ts +9 -1
  114. package/dist/transaction/transport/httpClient.js +1 -0
  115. package/dist/transaction/transport/httpTransport.js +156 -44
  116. package/dist/transaction/transport/wsTransport.d.ts +2 -4
  117. package/dist/transaction/transport/wsTransport.js +16 -10
  118. package/dist/transaction/types/streams.d.ts +10 -0
  119. package/dist/transaction/utils/duration.d.ts +25 -0
  120. package/dist/transaction/utils/duration.js +32 -0
  121. package/dist/transaction/wire/accountResponses.d.ts +69 -0
  122. package/dist/transaction/wire/accountResponses.js +36 -1
  123. package/dist/transaction/wire/auth.d.ts +9 -2
  124. package/dist/transaction/wire/auth.js +7 -1
  125. package/dist/transaction/wire/claims.d.ts +164 -97
  126. package/dist/transaction/wire/claims.js +126 -28
  127. package/dist/transaction/wire/commit.d.ts +1 -1
  128. package/dist/transaction/wire/feedEvent.d.ts +27 -0
  129. package/dist/transaction/wire/feedEvent.js +27 -1
  130. package/dist/transaction/wire/frames.d.ts +2 -2
  131. package/dist/transaction/wire/inboundFrames.d.ts +12 -2
  132. package/dist/transaction/wire/index.d.ts +10 -6
  133. package/dist/transaction/wire/index.js +12 -3
  134. package/dist/transaction/wire/modelMutations.d.ts +31 -0
  135. package/dist/transaction/wire/modelMutations.js +52 -0
  136. package/dist/transaction/wire/modelShape.d.ts +78 -0
  137. package/dist/transaction/wire/modelShape.js +74 -0
  138. package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
  139. package/dist/transactions/mutations/MutationQueue.js +55 -45
  140. package/dist/transactions/mutations/commitPayload.d.ts +3 -2
  141. package/dist/transactions/mutations/commitPayload.js +5 -5
  142. package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
  143. package/dist/transactions/mutations/deltaConfirmation.js +10 -8
  144. package/dist/transactions/mutations/replayValidation.d.ts +2 -1
  145. package/dist/transactions/mutations/replayValidation.js +3 -2
  146. package/dist/{core → views}/QueryView.d.ts +1 -1
  147. package/dist/{core → views}/QueryView.js +1 -1
  148. package/dist/{core → views}/ViewRegistry.d.ts +1 -1
  149. package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
  150. package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
  151. package/docs/agents.md +1 -1
  152. package/docs/api-keys.md +6 -6
  153. package/docs/api.md +5 -43
  154. package/docs/audit.md +4 -3
  155. package/docs/cli.md +11 -11
  156. package/docs/client-behavior.md +4 -4
  157. package/docs/concurrency-convention.md +28 -42
  158. package/docs/coordination.md +235 -83
  159. package/docs/data-sources.md +4 -4
  160. package/docs/debugging.md +34 -12
  161. package/docs/deployment.md +8 -8
  162. package/docs/examples/scoped-agent.md +3 -3
  163. package/docs/groups.md +57 -3
  164. package/docs/guarantees.md +37 -10
  165. package/docs/how-it-works.md +29 -5
  166. package/docs/idempotency.md +6 -6
  167. package/docs/identity.md +22 -23
  168. package/docs/index.md +8 -8
  169. package/docs/integration-guide.md +14 -3
  170. package/docs/mcp.md +7 -7
  171. package/docs/migration.md +34 -15
  172. package/docs/projects.md +1 -1
  173. package/docs/react.md +19 -8
  174. package/docs/sessions.md +1 -1
  175. package/docs/webhooks.md +9 -9
  176. package/llms.txt +4 -4
  177. package/package.json +13 -20
  178. package/dist/cli.cjs +0 -288600
  179. package/dist/testing/fixtures/bootstrap.d.ts +0 -49
  180. package/dist/testing/fixtures/bootstrap.js +0 -59
  181. package/dist/testing/fixtures/deltas.d.ts +0 -83
  182. package/dist/testing/fixtures/deltas.js +0 -136
  183. package/dist/testing/fixtures/httpResponses.d.ts +0 -70
  184. package/dist/testing/fixtures/httpResponses.js +0 -90
  185. package/dist/testing/fixtures/models.d.ts +0 -83
  186. package/dist/testing/fixtures/models.js +0 -272
  187. package/dist/testing/helpers/reactWrapper.d.ts +0 -69
  188. package/dist/testing/helpers/reactWrapper.js +0 -67
  189. package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
  190. package/dist/testing/helpers/syncEngineHarness.js +0 -73
  191. package/dist/testing/helpers/wait.d.ts +0 -30
  192. package/dist/testing/helpers/wait.js +0 -49
  193. package/dist/testing/index.d.ts +0 -23
  194. package/dist/testing/index.js +0 -33
  195. package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
  196. package/dist/testing/mocks/FakeDatabase.js +0 -10
  197. package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
  198. package/dist/testing/mocks/MockMutationExecutor.js +0 -186
  199. package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
  200. package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
  201. package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
  202. package/dist/testing/mocks/MockSyncContext.js +0 -72
  203. package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
  204. package/dist/testing/mocks/MockSyncStore.js +0 -171
  205. package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
  206. package/dist/testing/mocks/MockWebSocket.js +0 -118
  207. package/docs/interaction-model.md +0 -99
  208. /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
  209. /package/dist/{core → query}/QueryProcessor.js +0 -0
  210. /package/dist/{core/storeContract.js → storeContract.js} +0 -0
  211. /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
  212. /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
  213. /package/dist/{source → transaction}/footprint.d.ts +0 -0
  214. /package/dist/{source → transaction}/footprint.js +0 -0
  215. /package/dist/{core → views}/ViewRegistry.js +0 -0
@@ -6,9 +6,12 @@ import { ABLO_HOSTED_HTTP_BASE_URL } from '../auth/hostedEndpoints.js';
6
6
  // enforced one.
7
7
  import { z } from 'zod';
8
8
  import { commitRequestSchema, commitReceiptSchema } from '../wire/commit.js';
9
- import { claimRequestSchema, claimHeartbeatRequestSchema, listQuerySchema, claimStateSchema, claimAcquiredResponseSchema, claimQueuedResponseSchema, claimHeartbeatReplySchema, } from '../wire/claims.js';
9
+ import { claimRequestSchema, claimHeartbeatRequestSchema, listQuerySchema, claimStateSchema, claimAcquiredResponseSchema, claimQueuedResponseSchema, claimHeartbeatReplySchema, claimHeartbeatBatchReplySchema, claimListQuerySchema, claimListResponseSchema, claimReorderRequestSchema, claimReorderReplySchema, claimReleaseReplySchema, } from '../wire/claims.js';
10
10
  import { modelReadResponseSchema, modelListResponseSchema } from '../wire/modelResponses.js';
11
- import { ephemeralKeyRequestSchema, capabilityRequestSchema } from '../wire/auth.js';
11
+ import { modelMutationRequestSchema } from '../wire/modelMutations.js';
12
+ import { logListResponseSchema, logQuerySchema } from '../wire/feedEvent.js';
13
+ import { schemaReadResponseSchema } from '../wire/accountResponses.js';
14
+ import { ephemeralKeyRequestSchema, capabilityRequestSchema, capabilityMintResponseSchema, } from '../wire/auth.js';
12
15
  import { EphemeralKeyResponseSchema } from '../auth/schemas.js';
13
16
  function fieldSchema(f) {
14
17
  switch (f.type) {
@@ -61,6 +64,16 @@ const modelParam = () => ({
61
64
  description: 'A model name from your pushed schema, e.g. `task`.',
62
65
  });
63
66
  const genericRow = () => ({ type: 'object', additionalProperties: true });
67
+ /**
68
+ * The body of a model-scoped write, derived from the schema the server
69
+ * validates it against.
70
+ *
71
+ * It was `genericRow()` — "an object" — which described the record flat and
72
+ * omitted the envelope entirely. A client built from that document sent the
73
+ * row's fields at the top level, where the server looks for `data`, and every
74
+ * one of them went missing at once.
75
+ */
76
+ const mutationBody = () => jsonBody(derive(modelMutationRequestSchema, 'input'));
64
77
  /**
65
78
  * Fill in `data` on a derived model-response schema.
66
79
  *
@@ -81,11 +94,30 @@ function withGenericRows(derived) {
81
94
  : genericRow();
82
95
  return { ...derived, properties };
83
96
  }
84
- /** Query parameters derived from the schema the route reads them with. */
85
- function listQueryParams() {
86
- const props = (derive(listQuerySchema, 'input').properties ?? {});
87
- return Object.entries(props).map(([name, schema]) => ({ name, in: 'query', schema }));
97
+ /**
98
+ * Query parameters derived from the schema the route reads them with.
99
+ *
100
+ * Taking the schema as an argument rather than closing over one is what lets the
101
+ * claim listing publish its filters too: `GET /v1/claims` accepts six, and a
102
+ * hand-written parameter list beside them would be the drifting copy this file
103
+ * exists to avoid.
104
+ */
105
+ function queryParams(schema) {
106
+ const props = (derive(schema, 'input').properties ?? {});
107
+ return Object.entries(props).map(([name, s]) => ({ name, in: 'query', schema: s }));
88
108
  }
109
+ /**
110
+ * Both release routes answer in one shape. `released` distinguishes "this call
111
+ * ended your lease" from "there was nothing of yours to end" — both success,
112
+ * and a retry after a lost response deserves to know which it got.
113
+ */
114
+ const releaseResp = () => jsonResp('Released', derive(claimReleaseReplySchema, 'output'));
115
+ const claimIdParam = () => ({
116
+ name: 'claimId',
117
+ in: 'path',
118
+ required: true,
119
+ schema: { type: 'string' },
120
+ });
89
121
  /**
90
122
  * The `POST /v1/commits` body, derived from {@link commitRequestSchema} — the
91
123
  * same schema the server validates the request against.
@@ -115,15 +147,14 @@ function envelope(options, description, paths, schemas) {
115
147
  };
116
148
  }
117
149
  /**
118
- * The protocol reference: the five route templates the server actually serves,
119
- * plus `/v1/commits`.
150
+ * The protocol reference: the route templates the server actually serves.
120
151
  *
121
152
  * This takes no schema, and that is the point. The server registers one
122
- * parameterised route family (`/api/v1/models/:model/...`), so the API is five
123
- * routes no matter how many models a tenant defines — and a spec that cannot see
124
- * a schema cannot grow with one. It is publishable once, identical for every
125
- * caller, and stable across every schema push: the document a Python or Go
126
- * client is generated from, and the surface an agent is handed.
153
+ * parameterised route family (`/api/v1/models/:model/...`), so the surface is
154
+ * the same size no matter how many models a tenant defines — and a spec that
155
+ * cannot see a schema cannot grow with one. It is publishable once, identical
156
+ * for every caller, and stable across every schema push: the document a Python
157
+ * or Go client is generated from, and the surface an agent is handed.
127
158
  *
128
159
  * Payload shapes are generic here by design. A caller that wants them typed
129
160
  * either reads the schema at runtime or generates the per-tenant expansion with
@@ -131,25 +162,46 @@ function envelope(options, description, paths, schemas) {
131
162
  */
132
163
  export function abloOpenApi(options = {}) {
133
164
  const rowResp = jsonResp('The row, with the watermark it was read at and who holds it.', withGenericRows(derive(modelReadResponseSchema, 'output')));
134
- const writeBody = jsonBody(genericRow());
135
165
  const tags = ['models'];
136
166
  const paths = {
137
167
  '/v1/models/{model}': {
138
168
  get: {
139
169
  tags,
140
170
  summary: 'List rows of a model',
141
- parameters: [modelParam(), ...listQueryParams()],
171
+ parameters: [modelParam(), ...queryParams(listQuerySchema)],
142
172
  responses: {
143
173
  '200': jsonResp('A page of rows. `next_cursor` feeds `starting_after` on the next ' +
144
174
  'call; `stamp` is the watermark the page was read at.', withGenericRows(derive(modelListResponseSchema, 'output'))),
145
175
  },
146
176
  },
147
- post: { tags, summary: 'Create a row', parameters: [modelParam()], requestBody: writeBody, responses: { '200': commitReceipt() } },
177
+ post: {
178
+ tags,
179
+ summary: 'Create a row',
180
+ description: 'The record travels in `data`; `id` is yours to choose — see the ' +
181
+ 'field for the derivation that makes a retry idempotent.',
182
+ parameters: [modelParam()],
183
+ requestBody: mutationBody(),
184
+ responses: { '200': commitReceipt() },
185
+ },
148
186
  },
149
187
  '/v1/models/{model}/{id}': {
150
188
  get: { tags, summary: 'Retrieve a row', parameters: [modelParam(), idParam()], responses: { '200': rowResp } },
151
- patch: { tags, summary: 'Update a row', parameters: [modelParam(), idParam()], requestBody: writeBody, responses: { '200': commitReceipt() } },
152
- delete: { tags, summary: 'Delete a row', parameters: [modelParam(), idParam()], responses: { '200': commitReceipt() } },
189
+ patch: {
190
+ tags,
191
+ summary: 'Update a row',
192
+ description: 'Pass `claim` and `readAt` together: the lease says nobody else is ' +
193
+ 'writing, the watermark says the row has not moved since you read it.',
194
+ parameters: [modelParam(), idParam()],
195
+ requestBody: mutationBody(),
196
+ responses: { '200': commitReceipt() },
197
+ },
198
+ delete: {
199
+ tags,
200
+ summary: 'Delete a row',
201
+ parameters: [modelParam(), idParam()],
202
+ requestBody: { ...mutationBody(), required: false },
203
+ responses: { '200': commitReceipt() },
204
+ },
153
205
  },
154
206
  '/v1/models/{model}/{id}/claim': {
155
207
  post: {
@@ -165,7 +217,12 @@ export function abloOpenApi(options = {}) {
165
217
  '`GET /v1/claims/{claimId}` for the grant.', derive(claimQueuedResponseSchema, 'output')),
166
218
  },
167
219
  },
168
- delete: { tags: ['claims'], summary: 'Release a claim', parameters: [modelParam(), idParam()], responses: { '200': jsonResp('Released', { type: 'object' }) } },
220
+ delete: {
221
+ tags: ['claims'],
222
+ summary: 'Release a claim',
223
+ parameters: [modelParam(), idParam()],
224
+ responses: { '200': releaseResp() },
225
+ },
169
226
  },
170
227
  '/v1/models/{model}/{id}/claim/heartbeat': {
171
228
  post: {
@@ -179,7 +236,17 @@ export function abloOpenApi(options = {}) {
179
236
  },
180
237
  },
181
238
  '/v1/models/{model}/{id}/claim/reorder': {
182
- post: { tags: ['claims'], summary: 'Reorder the wait-line (privileged)', parameters: [modelParam(), idParam()], responses: { '200': jsonResp('Reordered', { type: 'object' }) } },
239
+ post: {
240
+ tags: ['claims'],
241
+ summary: 'Reorder the wait-line (privileged)',
242
+ description: 'Name the waiters you want at the front, in the order you want them. ' +
243
+ 'Waiters you leave out keep their relative places behind them.',
244
+ parameters: [modelParam(), idParam()],
245
+ requestBody: optionalJsonBody(derive(claimReorderRequestSchema, 'input')),
246
+ responses: {
247
+ '200': jsonResp('Reordered', derive(claimReorderReplySchema, 'output')),
248
+ },
249
+ },
183
250
  },
184
251
  '/v1/ephemeral_keys': {
185
252
  post: {
@@ -194,6 +261,44 @@ export function abloOpenApi(options = {}) {
194
261
  responses: { '200': jsonResp('The minted credential', derive(EphemeralKeyResponseSchema, 'output')) },
195
262
  },
196
263
  },
264
+ '/v1/claims': {
265
+ get: {
266
+ tags: ['claims'],
267
+ summary: 'List who holds what, and who waits',
268
+ description: 'The coordination view: scope to a row with `model` and `id`, to a ' +
269
+ 'participant with `actorId`, `actorKind`, `onBehalfOfId` or ' +
270
+ '`capabilityId`, or combine them. `queue` is populated only when the ' +
271
+ 'request names both `model` and `id` — a wait line belongs to one row.',
272
+ parameters: queryParams(claimListQuerySchema),
273
+ responses: {
274
+ '200': jsonResp('Live claims, and the wait line behind the named row.', derive(claimListResponseSchema, 'output')),
275
+ },
276
+ },
277
+ post: {
278
+ tags: ['claims'],
279
+ summary: 'Claim a row named in the body',
280
+ description: 'The same operation as `POST /v1/models/{model}/{id}/claim`, with the ' +
281
+ 'row in `target` instead of the URL. Answers identically.',
282
+ requestBody: jsonBody(derive(claimRequestSchema, 'input')),
283
+ responses: {
284
+ '201': jsonResp('The lease is yours.', derive(claimAcquiredResponseSchema, 'output')),
285
+ '202': jsonResp('Already held, and you asked to queue. You are in line at `position`.', derive(claimQueuedResponseSchema, 'output')),
286
+ },
287
+ },
288
+ },
289
+ '/v1/claims/heartbeat': {
290
+ post: {
291
+ tags: ['claims'],
292
+ summary: 'Heartbeat every lease you hold, in one request',
293
+ description: 'One round trip per cadence for a worker holding many rows, instead of ' +
294
+ 'one per row. Takes only `ttl`; the leases are whichever ones your ' +
295
+ 'credential holds on this plane.',
296
+ requestBody: optionalJsonBody(derive(claimHeartbeatRequestSchema, 'input')),
297
+ responses: {
298
+ '200': jsonResp('One ack per lease extended.', derive(claimHeartbeatBatchReplySchema, 'output')),
299
+ },
300
+ },
301
+ },
197
302
  '/v1/claims/{claimId}': {
198
303
  get: {
199
304
  tags: ['claims'],
@@ -201,7 +306,7 @@ export function abloOpenApi(options = {}) {
201
306
  description: 'How a caller without a persistent connection learns its queued claim ' +
202
307
  'was granted. `position` is advisory — a privileged reorder can move it ' +
203
308
  'up — so branch on `status`, never on position.',
204
- parameters: [{ name: 'claimId', in: 'path', required: true, schema: { type: 'string' } }],
309
+ parameters: [claimIdParam()],
205
310
  responses: { '200': jsonResp('The claim state', derive(claimStateSchema, 'output')) },
206
311
  },
207
312
  delete: {
@@ -210,8 +315,24 @@ export function abloOpenApi(options = {}) {
210
315
  description: 'The same call for both: releasing a held lease and abandoning a queued ' +
211
316
  'position are one operation, because a queue entry is a lease in a ' +
212
317
  'different state.',
213
- parameters: [{ name: 'claimId', in: 'path', required: true, schema: { type: 'string' } }],
214
- responses: { '200': jsonResp('Released', { type: 'object' }) },
318
+ parameters: [claimIdParam()],
319
+ responses: { '200': releaseResp() },
320
+ },
321
+ },
322
+ '/v1/claims/{claimId}/heartbeat': {
323
+ post: {
324
+ tags: ['claims'],
325
+ summary: 'Heartbeat a claim by id — held or queued',
326
+ description: 'The beat a waiter needs: a queued caller holds nothing but the ' +
327
+ '`claimId` it was handed at enqueue, and an entry that stops beating ' +
328
+ 'drops out of the line on TTL. The reply doubles as the wait poll — ' +
329
+ '`queued` means still in line, `held` means the grant landed, at which ' +
330
+ 'point `GET /v1/claims/{claimId}` carries the fence token.',
331
+ parameters: [claimIdParam()],
332
+ requestBody: optionalJsonBody(derive(claimHeartbeatRequestSchema, 'input')),
333
+ responses: {
334
+ '200': jsonResp('Lease extended, or queued slot refreshed.', derive(claimHeartbeatReplySchema, 'output')),
335
+ },
215
336
  },
216
337
  },
217
338
  '/v1/capabilities': {
@@ -221,7 +342,14 @@ export function abloOpenApi(options = {}) {
221
342
  description: 'A scoped, revocable grant. Narrow by default: an agent or system ' +
222
343
  'capability must name its `syncGroups` and `operations`.',
223
344
  requestBody: jsonBody(derive(capabilityRequestSchema, 'input')),
224
- responses: { '200': jsonResp('The minted capability', { type: 'object' }) },
345
+ responses: {
346
+ // 201, not 200. The reference said 200 while the route has always
347
+ // answered 201, so a generated client checking the documented code
348
+ // failed on every successful mint.
349
+ '201': jsonResp('The minted capability. `token` is the credential — carry it as the ' +
350
+ 'Bearer token on every other call. `scope` is what was minted, ' +
351
+ 'which is not always what was asked for.', derive(capabilityMintResponseSchema, 'output')),
352
+ },
225
353
  },
226
354
  },
227
355
  '/v1/capabilities/{id}': {
@@ -246,6 +374,39 @@ export function abloOpenApi(options = {}) {
246
374
  responses: { '200': jsonResp('The rotated capability', { type: 'object' }) },
247
375
  },
248
376
  },
377
+ '/v1/schema': {
378
+ get: {
379
+ tags: ['schema'],
380
+ summary: 'What the models look like',
381
+ description: "The schema deployed on your credential's plane: every model with its " +
382
+ 'fields, their types, and its relations. Read this when you have no ' +
383
+ 'local schema declaration to read types from — it is what makes a ' +
384
+ 'field typo a local check rather than a rejected write. Each model ' +
385
+ 'carries a content `hash` that moves only when its shape does, so read ' +
386
+ 'the shape once and poll the hashes after: a refetch only ever answers ' +
387
+ 'a push.',
388
+ responses: {
389
+ '200': jsonResp('The deployed schema, or `active: false` when nothing is pushed.', derive(schemaReadResponseSchema, 'output')),
390
+ },
391
+ },
392
+ },
393
+ '/v1/logs': {
394
+ get: {
395
+ tags: ['logs'],
396
+ summary: 'Tail what changed',
397
+ description: 'How a caller without a socket learns what its peers did. Omit ' +
398
+ '`after` for the most recent entries, then copy each page\'s ' +
399
+ '`next_cursor` back as `after` to walk forward. Scope is taken from ' +
400
+ 'your key — organization, plane and sync groups — so a caller cannot ' +
401
+ 'widen what it sees by asking.',
402
+ parameters: queryParams(logQuerySchema),
403
+ responses: {
404
+ '200': jsonResp('A page of the feed, oldest first. Entries are discriminated on ' +
405
+ '`object`, so a reader that meets an entry kind it does not know ' +
406
+ 'can skip it and keep paging.', derive(logListResponseSchema, 'output')),
407
+ },
408
+ },
409
+ },
249
410
  '/v1/commits': {
250
411
  post: {
251
412
  tags: ['commits'],
@@ -290,7 +451,7 @@ export function schemaToOpenApi(schema, options = {}) {
290
451
  get: {
291
452
  tags: [key],
292
453
  summary: `List ${key}`,
293
- parameters: listQueryParams(),
454
+ parameters: queryParams(listQuerySchema),
294
455
  responses: {
295
456
  '200': jsonResp('List of rows', {
296
457
  type: 'object',
@@ -96,7 +96,13 @@ export interface BelongsToOptions {
96
96
  declare const __relationType: unique symbol;
97
97
  declare const __relationTarget: unique symbol;
98
98
  declare const __relationField: unique symbol;
99
- export type RelationType = 'belongsTo' | 'hasMany' | 'hasOne';
99
+ /**
100
+ * The three edges the engine resolves. Inferred from `wire/modelShape.ts`
101
+ * rather than restated: the set is reported to callers by `GET /api/schema`,
102
+ * so it is protocol, and protocol has one definition.
103
+ */
104
+ export type { RelationType } from '../wire/modelShape.js';
105
+ import type { RelationType } from '../wire/modelShape.js';
100
106
  /**
101
107
  * A relation definition, carrying its type information at both the type and
102
108
  * runtime level.
@@ -196,4 +202,3 @@ export declare const relation: {
196
202
  */
197
203
  readonly hasOne: <Target extends string, Field extends string>(target: Target, foreignKey: Field) => RelationDef<"hasOne", Target, Field>;
198
204
  };
199
- export {};
@@ -23,6 +23,7 @@ import { z } from 'zod';
23
23
  import type { ModelDef, RelationRecord } from './model.js';
24
24
  import type { RelationDef } from './relation.js';
25
25
  import type { IdentityRole } from './roles.js';
26
+ import { type FieldRef } from './fieldRef.js';
26
27
  export { type IdentityRole, type IdentityRoleSource, type IdentityContext, type EntityRole, type EntityRoleSource, type EntityContext, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, intersectRequestedWithAllowed, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './roles.js';
27
28
  /** The set of built-in casing conventions supported by `defineSchema`. */
28
29
  export type CasingConvention = 'snake_case' | 'camelCase';
@@ -150,6 +151,23 @@ export interface BaseModelMethods {
150
151
  export interface Schema<S extends SchemaRecord = SchemaRecord> {
151
152
  /** The raw model definitions */
152
153
  readonly models: S;
154
+ /**
155
+ * Every declared field as a {@link FieldRef} — the field as a value rather
156
+ * than a quoted name: `schema.fields.tasks.status`.
157
+ *
158
+ * A surface that names a field takes one of these instead of a string, so a
159
+ * name that does not exist stops compiling and a rename is a compile error at
160
+ * every use. Claims are the first caller; anything else that has to say
161
+ * "which field" should take a reference for the same reason.
162
+ *
163
+ * Base fields (`id`, `createdAt`, …) are not here: the schema does not
164
+ * declare them, and a caller naming one means the row, not a part of it.
165
+ */
166
+ readonly fields: {
167
+ readonly [K in keyof S]: S[K] extends ModelDef<infer Shape> ? {
168
+ readonly [F in Extract<keyof Shape, string>]: FieldRef;
169
+ } : never;
170
+ };
153
171
  /** Zod schemas with base fields merged in */
154
172
  readonly validators: {
155
173
  readonly [K in keyof S]: S[K] extends ModelDef<infer Shape> ? z.ZodObject<Shape & typeof baseFieldsSchema.shape> : never;
@@ -321,4 +339,13 @@ export type UpdateValue<S extends Schema, ModelName extends keyof S['models']> =
321
339
  export interface DeleteId<S extends Schema, ModelName extends keyof S['models']> {
322
340
  id: string;
323
341
  }
342
+ /**
343
+ * One {@link FieldRef} per declared field, keyed by model then field.
344
+ *
345
+ * Every construction of a {@link Schema} goes through here — `defineSchema`,
346
+ * the `selectModels`/`omitModels` projections, and the deserializer — so a
347
+ * projection carries references for exactly the models it kept and a schema
348
+ * rebuilt from JSON carries them at all.
349
+ */
350
+ export declare function buildFieldRefs(models: Record<string, ModelDef>): Record<string, Record<string, FieldRef>>;
324
351
  export declare function defineSchema<const S extends SchemaRecord>(models: S, options?: DefineSchemaOptions): Schema<S>;
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import { z } from 'zod';
23
23
  import { AbloValidationError } from '../errors.js';
24
+ import { fieldRef } from './fieldRef.js';
24
25
  import { scopeSchema, grantsRefSchema } from './roles.js';
25
26
  // Sync-group roles (identity and entity) are defined in `./roles.js` and
26
27
  // re-exported here so they can also be imported from this module. See
@@ -153,6 +154,24 @@ function assertRoundTrippableCamelCase(modelName, fieldName) {
153
154
  }
154
155
  }
155
156
  }
157
+ /**
158
+ * One {@link FieldRef} per declared field, keyed by model then field.
159
+ *
160
+ * Every construction of a {@link Schema} goes through here — `defineSchema`,
161
+ * the `selectModels`/`omitModels` projections, and the deserializer — so a
162
+ * projection carries references for exactly the models it kept and a schema
163
+ * rebuilt from JSON carries them at all.
164
+ */
165
+ export function buildFieldRefs(models) {
166
+ const refs = {};
167
+ for (const [model, def] of Object.entries(models)) {
168
+ const perField = {};
169
+ for (const field of Object.keys(def.shape))
170
+ perField[field] = fieldRef(model, field);
171
+ refs[model] = perField;
172
+ }
173
+ return refs;
174
+ }
156
175
  export function defineSchema(models, options) {
157
176
  // Build validators with base fields merged in, and resolve defaults for
158
177
  // `typename` and `persist.store` so downstream code (the loader and the
@@ -228,6 +247,7 @@ export function defineSchema(models, options) {
228
247
  // already part of ModelDef, so the shape is structurally unchanged.
229
248
  models: resolvedModels,
230
249
  validators: validators,
250
+ fields: buildFieldRefs(resolvedModels),
231
251
  identityRoles: options?.identityRoles ?? [],
232
252
  sessionSettings: options?.sessionSettings ?? {},
233
253
  };
@@ -111,8 +111,8 @@ export declare const durableCommitEnvelopeSchema: z.ZodObject<{
111
111
  }>>;
112
112
  }, z.core.$strip>]>>>>;
113
113
  track: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
114
- model: z.ZodString;
115
114
  id: z.ZodString;
115
+ model: z.ZodString;
116
116
  readAt: z.ZodOptional<z.ZodNumber>;
117
117
  }, z.core.$strip>, z.ZodObject<{
118
118
  group: z.ZodString;
@@ -67,8 +67,8 @@ export declare const pendingWriteSchema: z.ZodUnion<readonly [z.ZodObject<{
67
67
  }>>;
68
68
  }, z.core.$strip>]>>>>;
69
69
  track: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
70
- model: z.ZodString;
71
70
  id: z.ZodString;
71
+ model: z.ZodString;
72
72
  readAt: z.ZodOptional<z.ZodNumber>;
73
73
  }, z.core.$strip>, z.ZodObject<{
74
74
  group: z.ZodString;
@@ -1,5 +1,5 @@
1
1
  import type { HttpClientConfig } from './httpOptions.js';
2
- import type { CommitResource, HttpClaimApi, ModelReadOptions, CreateSessionParams, AbloSession } from '../resources/httpResources.js';
2
+ import type { CommitResource, HttpClaimApi, HttpClaimsResource, ModelReadOptions, CreateSessionParams, AbloSession } from '../resources/httpResources.js';
3
3
  import type { ModelCreateParams, ModelDeleteParams, ServerReadOptions, ModelRetrieveParams, ModelTrackParams, ModelTrackResult, ModelUpdateParams } from '../resources/modelOperations.js';
4
4
  import type { Schema, SchemaRecord, InferModel, InferCreate } from '../schema/schema.js';
5
5
  import type { ModelUpdater, ContentionOptions } from '../resources/functionalUpdate.js';
@@ -108,6 +108,14 @@ export type AbloHttpClient<S extends SchemaRecord> = {
108
108
  /** Replays every pending durable HTTP write in seal order and waits for settlement. */
109
109
  waitForFlush(): Promise<void>;
110
110
  readonly commits: CommitResource;
111
+ /**
112
+ * Claim-ticket operations keyed by `claimId`: `retrieve` polls a queued
113
+ * ticket to its grant, `heartbeat` keeps one lease (held or queued) alive,
114
+ * `heartbeatAll` beats every lease this identity holds in one round trip.
115
+ * The id comes from `AbloClaimedError('claim_queued')`, which carries it on
116
+ * `error.claims`.
117
+ */
118
+ readonly claims: HttpClaimsResource;
111
119
  dispose(): Promise<void>;
112
120
  /** Resolves the bearer credential this client authenticates with, or `null` if none is set. */
113
121
  getAuthToken(): Promise<string | null>;
@@ -29,6 +29,7 @@ const PROTOCOL_MEMBERS = new Set([
29
29
  'waitForFlush',
30
30
  'dispose',
31
31
  'commits',
32
+ 'claims',
32
33
  'getAuthToken',
33
34
  'sessions',
34
35
  ]);