@abloatai/transaction 0.48.0 → 0.49.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 (257) hide show
  1. package/CONVENTIONS.md +7 -7
  2. package/README.md +1 -1
  3. package/dist/ai-sdk/modelTools.d.ts +1 -1
  4. package/dist/ai-sdk/modelTools.js +1 -1
  5. package/dist/ai-sdk/updateTool.d.ts +2 -2
  6. package/dist/ai-sdk/updateTool.d.ts.map +1 -1
  7. package/dist/auth/bootstrapScope.d.ts +1 -1
  8. package/dist/auth/capability.d.ts +32 -10
  9. package/dist/auth/capability.d.ts.map +1 -1
  10. package/dist/auth/capability.js +9 -3
  11. package/dist/auth/capability.js.map +1 -1
  12. package/dist/auth/capabilityLifecycle.d.ts +2 -2
  13. package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
  14. package/dist/auth/capabilityLifecycle.js +3 -2
  15. package/dist/auth/capabilityLifecycle.js.map +1 -1
  16. package/dist/auth/identity.d.ts +3 -0
  17. package/dist/auth/identity.d.ts.map +1 -1
  18. package/dist/auth/identity.js +13 -1
  19. package/dist/auth/identity.js.map +1 -1
  20. package/dist/auth/index.d.ts +2 -2
  21. package/dist/auth/index.d.ts.map +1 -1
  22. package/dist/auth/index.js +1 -1
  23. package/dist/auth/index.js.map +1 -1
  24. package/dist/auth/schemas.d.ts +18 -1
  25. package/dist/auth/schemas.d.ts.map +1 -1
  26. package/dist/auth/schemas.js +3 -1
  27. package/dist/auth/schemas.js.map +1 -1
  28. package/dist/auth/sessionMint.d.ts.map +1 -1
  29. package/dist/auth/sessionMint.js +0 -1
  30. package/dist/auth/sessionMint.js.map +1 -1
  31. package/dist/commitRecordRuntime.d.ts +30 -0
  32. package/dist/commitRecordRuntime.d.ts.map +1 -0
  33. package/dist/commitRecordRuntime.js +188 -0
  34. package/dist/commitRecordRuntime.js.map +1 -0
  35. package/dist/coordination/index.d.ts +3 -3
  36. package/dist/coordination/index.d.ts.map +1 -1
  37. package/dist/coordination/index.js +2 -2
  38. package/dist/coordination/index.js.map +1 -1
  39. package/dist/coordination/locator.d.ts +5 -0
  40. package/dist/coordination/locator.d.ts.map +1 -1
  41. package/dist/coordination/locator.js +12 -0
  42. package/dist/coordination/locator.js.map +1 -1
  43. package/dist/coordination/schema.d.ts +205 -15
  44. package/dist/coordination/schema.d.ts.map +1 -1
  45. package/dist/coordination/schema.js +88 -47
  46. package/dist/coordination/schema.js.map +1 -1
  47. package/dist/coordination/targetConflict.js +1 -1
  48. package/dist/durableWrites.d.ts +3 -3
  49. package/dist/durableWrites.d.ts.map +1 -1
  50. package/dist/durableWrites.js +1 -1
  51. package/dist/errorCodes.d.ts +20 -1
  52. package/dist/errorCodes.d.ts.map +1 -1
  53. package/dist/errorCodes.js +44 -10
  54. package/dist/errorCodes.js.map +1 -1
  55. package/dist/errorObservation.d.ts +82 -0
  56. package/dist/errorObservation.d.ts.map +1 -0
  57. package/dist/errorObservation.js +101 -0
  58. package/dist/errorObservation.js.map +1 -0
  59. package/dist/errors.d.ts +2 -0
  60. package/dist/errors.d.ts.map +1 -1
  61. package/dist/errors.js +6 -1
  62. package/dist/errors.js.map +1 -1
  63. package/dist/index.d.ts +5 -2
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +1 -1
  66. package/dist/index.js.map +1 -1
  67. package/dist/internal/read-set.d.ts +8 -0
  68. package/dist/internal/read-set.d.ts.map +1 -0
  69. package/dist/internal/read-set.js +7 -0
  70. package/dist/internal/read-set.js.map +1 -0
  71. package/dist/logger.d.ts +1 -1
  72. package/dist/observability.d.ts +1 -1
  73. package/dist/observability.js +1 -1
  74. package/dist/readSetContext.d.ts +54 -0
  75. package/dist/readSetContext.d.ts.map +1 -0
  76. package/dist/readSetContext.js +98 -0
  77. package/dist/readSetContext.js.map +1 -0
  78. package/dist/resources/functionalUpdate.d.ts +13 -3
  79. package/dist/resources/functionalUpdate.d.ts.map +1 -1
  80. package/dist/resources/functionalUpdate.js.map +1 -1
  81. package/dist/resources/httpResources.d.ts +23 -7
  82. package/dist/resources/httpResources.d.ts.map +1 -1
  83. package/dist/resources/modelOperations.d.ts +2 -2
  84. package/dist/resources/modelOperations.js +1 -1
  85. package/dist/resources/mutationOptions.d.ts +1 -1
  86. package/dist/resources/mutationOptions.js +1 -1
  87. package/dist/resources/where.d.ts +1 -1
  88. package/dist/resources/where.js +1 -1
  89. package/dist/resources/writeOptionsSchema.d.ts +39 -4
  90. package/dist/resources/writeOptionsSchema.d.ts.map +1 -1
  91. package/dist/resources/writeOptionsSchema.js +12 -6
  92. package/dist/resources/writeOptionsSchema.js.map +1 -1
  93. package/dist/schema/index.js +1 -1
  94. package/dist/schema/index.js.map +1 -1
  95. package/dist/schema/openapi.d.ts.map +1 -1
  96. package/dist/schema/openapi.js +41 -7
  97. package/dist/schema/openapi.js.map +1 -1
  98. package/dist/server/commit.d.ts +7 -1
  99. package/dist/server/commit.d.ts.map +1 -1
  100. package/dist/server/index.d.ts +1 -1
  101. package/dist/server/index.d.ts.map +1 -1
  102. package/dist/server/index.js +1 -1
  103. package/dist/server/index.js.map +1 -1
  104. package/dist/source/adapters/kysely.d.ts.map +1 -1
  105. package/dist/source/adapters/kysely.js +11 -4
  106. package/dist/source/adapters/kysely.js.map +1 -1
  107. package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
  108. package/dist/source/adapters/kyselyMutationCore.js +14 -2
  109. package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
  110. package/dist/source/contract.d.ts +2 -0
  111. package/dist/source/contract.d.ts.map +1 -1
  112. package/dist/source/types.d.ts +1 -1
  113. package/dist/testing/fixtures/httpResponses.d.ts +30 -1
  114. package/dist/testing/fixtures/httpResponses.d.ts.map +1 -1
  115. package/dist/testing/fixtures/httpResponses.js +48 -1
  116. package/dist/testing/fixtures/httpResponses.js.map +1 -1
  117. package/dist/transactionLayer.d.ts +1 -1
  118. package/dist/transactions/{settlement → confirmation}/commitEnvelope.d.ts +4 -2
  119. package/dist/transactions/confirmation/commitEnvelope.d.ts.map +1 -0
  120. package/dist/transactions/{settlement → confirmation}/commitEnvelope.js +8 -3
  121. package/dist/transactions/confirmation/commitEnvelope.js.map +1 -0
  122. package/dist/transactions/confirmation/httpCommitEnvelope.d.ts.map +1 -0
  123. package/dist/transactions/confirmation/httpCommitEnvelope.js.map +1 -0
  124. package/dist/transactions/confirmation/idempotencyKey.d.ts.map +1 -0
  125. package/dist/transactions/confirmation/idempotencyKey.js.map +1 -0
  126. package/dist/transactions/{settlement → confirmation}/pendingWrite.d.ts +3 -2
  127. package/dist/transactions/confirmation/pendingWrite.d.ts.map +1 -0
  128. package/dist/transactions/confirmation/pendingWrite.js.map +1 -0
  129. package/dist/transport/commitFrames.d.ts +1 -0
  130. package/dist/transport/commitFrames.d.ts.map +1 -1
  131. package/dist/transport/commitFrames.js +8 -7
  132. package/dist/transport/commitFrames.js.map +1 -1
  133. package/dist/transport/connectionManager.d.ts +1 -1
  134. package/dist/transport/connectionManager.js +1 -1
  135. package/dist/transport/httpClient.d.ts +23 -9
  136. package/dist/transport/httpClient.d.ts.map +1 -1
  137. package/dist/transport/httpClient.js +134 -15
  138. package/dist/transport/httpClient.js.map +1 -1
  139. package/dist/transport/httpOptions.d.ts +9 -1
  140. package/dist/transport/httpOptions.d.ts.map +1 -1
  141. package/dist/transport/httpTransport.d.ts +3 -0
  142. package/dist/transport/httpTransport.d.ts.map +1 -1
  143. package/dist/transport/httpTransport.js +109 -22
  144. package/dist/transport/httpTransport.js.map +1 -1
  145. package/dist/transport/wsFrameHandlers.d.ts +3 -1
  146. package/dist/transport/wsFrameHandlers.d.ts.map +1 -1
  147. package/dist/transport/wsFrameHandlers.js +38 -17
  148. package/dist/transport/wsFrameHandlers.js.map +1 -1
  149. package/dist/transport/wsTransport.d.ts +3 -0
  150. package/dist/transport/wsTransport.d.ts.map +1 -1
  151. package/dist/transport/wsTransport.js +32 -1
  152. package/dist/transport/wsTransport.js.map +1 -1
  153. package/dist/utils/json.d.ts +1 -1
  154. package/dist/utils/json.js +1 -1
  155. package/dist/wire/accountResponses.d.ts +1 -1
  156. package/dist/wire/auth.d.ts +2 -2
  157. package/dist/wire/commit.d.ts +1652 -213
  158. package/dist/wire/commit.d.ts.map +1 -1
  159. package/dist/wire/commit.js +196 -132
  160. package/dist/wire/commit.js.map +1 -1
  161. package/dist/wire/delta.d.ts +1 -1
  162. package/dist/wire/errorEnvelope.d.ts +1 -0
  163. package/dist/wire/errorEnvelope.d.ts.map +1 -1
  164. package/dist/wire/errorEnvelope.js +1 -0
  165. package/dist/wire/errorEnvelope.js.map +1 -1
  166. package/dist/wire/feedCursor.d.ts +1 -1
  167. package/dist/wire/feedCursor.js +1 -1
  168. package/dist/wire/feedEvent.d.ts +3 -3
  169. package/dist/wire/frames.d.ts +8 -4
  170. package/dist/wire/frames.d.ts.map +1 -1
  171. package/dist/wire/frames.js +6 -3
  172. package/dist/wire/frames.js.map +1 -1
  173. package/dist/wire/inboundFrames.d.ts +44 -4
  174. package/dist/wire/inboundFrames.d.ts.map +1 -1
  175. package/dist/wire/index.d.ts +6 -4
  176. package/dist/wire/index.d.ts.map +1 -1
  177. package/dist/wire/index.js +6 -5
  178. package/dist/wire/index.js.map +1 -1
  179. package/dist/wire/modelMutations.d.ts +35 -0
  180. package/dist/wire/modelMutations.d.ts.map +1 -1
  181. package/dist/wire/modelMutations.js +9 -2
  182. package/dist/wire/modelMutations.js.map +1 -1
  183. package/dist/wire/modelResponses.d.ts +12 -1
  184. package/dist/wire/modelResponses.d.ts.map +1 -1
  185. package/dist/wire/modelResponses.js +12 -1
  186. package/dist/wire/modelResponses.js.map +1 -1
  187. package/package.json +12 -2
  188. package/src/ai-sdk/modelTools.ts +1 -1
  189. package/src/ai-sdk/updateTool.ts +2 -2
  190. package/src/auth/bootstrapScope.ts +1 -1
  191. package/src/auth/capability.ts +13 -4
  192. package/src/auth/capabilityLifecycle.ts +3 -2
  193. package/src/auth/identity.ts +16 -1
  194. package/src/auth/index.ts +2 -0
  195. package/src/auth/schemas.ts +3 -0
  196. package/src/auth/sessionMint.ts +0 -1
  197. package/src/commitRecordRuntime.ts +245 -0
  198. package/src/coordination/index.ts +24 -1
  199. package/src/coordination/locator.ts +17 -0
  200. package/src/coordination/schema.ts +114 -49
  201. package/src/coordination/targetConflict.ts +1 -1
  202. package/src/durableWrites.ts +3 -3
  203. package/src/errorCodes.ts +107 -11
  204. package/src/errorObservation.ts +121 -0
  205. package/src/errors.ts +5 -1
  206. package/src/index.ts +8 -1
  207. package/src/internal/read-set.ts +18 -0
  208. package/src/logger.ts +1 -1
  209. package/src/observability.ts +1 -1
  210. package/src/readSetContext.ts +164 -0
  211. package/src/resources/functionalUpdate.ts +16 -4
  212. package/src/resources/httpResources.ts +27 -8
  213. package/src/resources/modelOperations.ts +2 -2
  214. package/src/resources/mutationOptions.ts +1 -1
  215. package/src/resources/where.ts +1 -1
  216. package/src/resources/writeOptionsSchema.ts +30 -12
  217. package/src/schema/index.ts +1 -1
  218. package/src/schema/openapi.ts +48 -7
  219. package/src/server/commit.ts +7 -1
  220. package/src/server/index.ts +5 -1
  221. package/src/source/adapters/kysely.ts +12 -4
  222. package/src/source/adapters/kyselyMutationCore.ts +14 -2
  223. package/src/source/types.ts +1 -1
  224. package/src/testing/fixtures/httpResponses.ts +60 -1
  225. package/src/transactionLayer.ts +1 -1
  226. package/src/transactions/{settlement → confirmation}/commitEnvelope.ts +13 -3
  227. package/src/transport/commitFrames.ts +7 -5
  228. package/src/transport/connectionManager.ts +1 -1
  229. package/src/transport/httpClient.ts +256 -29
  230. package/src/transport/httpOptions.ts +9 -1
  231. package/src/transport/httpTransport.ts +139 -27
  232. package/src/transport/wsFrameHandlers.ts +46 -8
  233. package/src/transport/wsTransport.ts +42 -1
  234. package/src/utils/json.ts +1 -1
  235. package/src/wire/commit.ts +248 -147
  236. package/src/wire/errorEnvelope.ts +1 -0
  237. package/src/wire/feedCursor.ts +1 -1
  238. package/src/wire/frames.ts +9 -4
  239. package/src/wire/index.ts +38 -5
  240. package/src/wire/modelMutations.ts +16 -2
  241. package/src/wire/modelResponses.ts +14 -1
  242. package/dist/transactions/settlement/commitEnvelope.d.ts.map +0 -1
  243. package/dist/transactions/settlement/commitEnvelope.js.map +0 -1
  244. package/dist/transactions/settlement/httpCommitEnvelope.d.ts.map +0 -1
  245. package/dist/transactions/settlement/httpCommitEnvelope.js.map +0 -1
  246. package/dist/transactions/settlement/idempotencyKey.d.ts.map +0 -1
  247. package/dist/transactions/settlement/idempotencyKey.js.map +0 -1
  248. package/dist/transactions/settlement/pendingWrite.d.ts.map +0 -1
  249. package/dist/transactions/settlement/pendingWrite.js.map +0 -1
  250. /package/dist/transactions/{settlement → confirmation}/httpCommitEnvelope.d.ts +0 -0
  251. /package/dist/transactions/{settlement → confirmation}/httpCommitEnvelope.js +0 -0
  252. /package/dist/transactions/{settlement → confirmation}/idempotencyKey.d.ts +0 -0
  253. /package/dist/transactions/{settlement → confirmation}/idempotencyKey.js +0 -0
  254. /package/dist/transactions/{settlement → confirmation}/pendingWrite.js +0 -0
  255. /package/src/transactions/{settlement → confirmation}/httpCommitEnvelope.ts +0 -0
  256. /package/src/transactions/{settlement → confirmation}/idempotencyKey.ts +0 -0
  257. /package/src/transactions/{settlement → confirmation}/pendingWrite.ts +0 -0
@@ -12,7 +12,7 @@ import type { ClaimTargetDetails } from './locator.js';
12
12
  * two places to land and no way to notice when it landed in one. It is one
13
13
  * definition here, and both authorities import it.
14
14
  *
15
- * It lives in the settlement core rather than the server because it is the rule
15
+ * It lives in the confirmation core rather than the server because it is the rule
16
16
  * itself, not a deployment of it: a claim authority running against a
17
17
  * developer's own database has to answer this question exactly the same way,
18
18
  * and a client that wants to predict a refusal before paying for the round trip
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Owns the persistence contract behind `Ablo({ durableWrites })` — a behavior
5
5
  * contract of methods, not a serialized shape, which is why it lives here
6
- * rather than in `transactions/settlement/`. The records that cross this port
6
+ * rather than in `transactions/confirmation/`. The records that cross this port
7
7
  * are owned by `pendingWriteSchema` there; this module never redescribes them.
8
8
  *
9
9
  * The engine consumes the port through `commitOutboxStore`, and the HTTP client
@@ -12,8 +12,8 @@
12
12
 
13
13
  import { z } from 'zod';
14
14
  import { AbloValidationError } from './errors.js';
15
- import type { CommitOutboxScope } from './transactions/settlement/commitEnvelope.js';
16
- import type { PendingWrite } from './transactions/settlement/pendingWrite.js';
15
+ import type { CommitOutboxScope } from './transactions/confirmation/commitEnvelope.js';
16
+ import type { PendingWrite } from './transactions/confirmation/pendingWrite.js';
17
17
 
18
18
  /**
19
19
  * Persistence port used by `Ablo({ durableWrites })`.
package/src/errorCodes.ts CHANGED
@@ -39,7 +39,7 @@ import { z } from 'zod';
39
39
  * error documentation and returned on the `Ablo-Version` response header, so a
40
40
  * consumer can detect when its expected contract has drifted from the server's.
41
41
  */
42
- export const ERROR_CONTRACT_VERSION = '2026-08-03';
42
+ export const ERROR_CONTRACT_VERSION = '2026-08-04';
43
43
 
44
44
  /** A coarse grouping of error codes, used to organize metrics and documentation. */
45
45
  export type ErrorCategory =
@@ -129,6 +129,36 @@ export interface ErrorCodeSpec {
129
129
  * this field can be left unset.
130
130
  */
131
131
  readonly recovery?: RecoveryClass;
132
+ /** Exhaustive sink and alert policy for this code. */
133
+ readonly observability: ErrorObservabilityPolicy;
134
+ }
135
+
136
+ export interface ErrorObservabilityPolicy {
137
+ readonly severity: 'info' | 'warning' | 'error' | 'fatal';
138
+ readonly sentry: 'log' | 'issue';
139
+ readonly pagingEligible: boolean;
140
+ readonly expectedVolume: 'low' | 'normal' | 'high';
141
+ readonly owner: 'platform' | 'product';
142
+ }
143
+
144
+ function observabilityPolicy(
145
+ category: ErrorCategory,
146
+ httpStatus: number | undefined,
147
+ operational = false,
148
+ ): ErrorObservabilityPolicy {
149
+ if ((httpStatus ?? 0) >= 500) {
150
+ return { severity: 'error', sentry: 'issue', pagingEligible: true, expectedVolume: 'low', owner: 'platform' };
151
+ }
152
+ if (operational) {
153
+ return { severity: 'warning', sentry: 'issue', pagingEligible: false, expectedVolume: 'low', owner: 'platform' };
154
+ }
155
+ return {
156
+ severity: category === 'auth' ? 'info' : 'warning',
157
+ sentry: 'log',
158
+ pagingEligible: false,
159
+ expectedVolume: category === 'auth' ? 'high' : 'normal',
160
+ owner: 'product',
161
+ };
132
162
  }
133
163
 
134
164
  const wire = (
@@ -136,8 +166,17 @@ const wire = (
136
166
  httpStatus: number,
137
167
  retryable: boolean,
138
168
  message: string,
139
- recovery?: RecoveryClass
140
- ): ErrorCodeSpec => ({ category, surface: 'wire', httpStatus, retryable, message, recovery });
169
+ recovery?: RecoveryClass,
170
+ operational = false,
171
+ ): ErrorCodeSpec => ({
172
+ category,
173
+ surface: 'wire',
174
+ httpStatus,
175
+ retryable,
176
+ message,
177
+ recovery,
178
+ observability: observabilityPolicy(category, httpStatus, operational),
179
+ });
141
180
 
142
181
  const client = (
143
182
  category: ErrorCategory,
@@ -147,6 +186,7 @@ const client = (
147
186
  category,
148
187
  surface: 'client',
149
188
  retryable: false,
189
+ observability: observabilityPolicy(category, undefined),
150
190
  message,
151
191
  ...(recovery !== undefined ? { recovery } : {}),
152
192
  });
@@ -234,6 +274,18 @@ export const ERROR_CODES = {
234
274
  'Your session has expired or is no longer valid. Sign in again to continue.',
235
275
  'session_expiry'
236
276
  ),
277
+ better_auth_request_failed: wire(
278
+ 'auth',
279
+ 400,
280
+ false,
281
+ 'The authentication request was rejected. Check the submitted credentials or request and try again.'
282
+ ),
283
+ better_auth_upstream_failed: wire(
284
+ 'server',
285
+ 500,
286
+ true,
287
+ 'The authentication service could not complete the request. Retry the request; if it continues to fail, contact support.'
288
+ ),
237
289
  // `jwt_invalid` is the general fallback; the codes below it split out specific
238
290
  // failure modes, so an integrator can tell a wrong JWKS registration from a
239
291
  // token with no organization claim from a wrong audience, instead of getting
@@ -361,25 +413,37 @@ export const ERROR_CODES = {
361
413
  'permission',
362
414
  403,
363
415
  false,
364
- 'The database role Ablo connects with is a superuser or has `BYPASSRLS`, so Postgres will not enforce row-level security for it. Connect with a role that is subject to RLS.'
416
+ 'The database role Ablo connects with is a superuser or has `BYPASSRLS`, so Postgres will not enforce row-level security for it. Connect with a role that is subject to RLS.',
417
+ undefined,
418
+ true,
365
419
  ),
366
420
  database_role_unreadable: wire(
367
421
  'permission',
368
422
  403,
369
423
  false,
370
- 'Ablo could not introspect the database role it connects with, so it cannot verify that row-level security is enforced.'
424
+ 'Ablo could not introspect the database role it connects with, so it cannot verify that row-level security is enforced.',
425
+ undefined,
426
+ true,
371
427
  ),
372
428
  database_tables_unforced_rls: wire(
373
429
  'permission',
374
430
  403,
375
431
  false,
376
- 'Some synced tables do not have `FORCE ROW LEVEL SECURITY` applied, so the table owner can bypass row isolation. Run `ALTER TABLE ... FORCE ROW LEVEL SECURITY` on each synced table.'
432
+ 'Some synced tables do not have `FORCE ROW LEVEL SECURITY` applied, so the table owner can bypass row isolation. Run `ALTER TABLE ... FORCE ROW LEVEL SECURITY` on each synced table.',
433
+ undefined,
434
+ true,
377
435
  ),
378
436
  database_host_not_allowed: wire(
379
437
  'permission',
380
438
  403,
381
439
  false,
382
- "The database host resolves to a private, loopback, or link-local address, which Ablo's servers will not connect to directly. Use a publicly resolvable direct endpoint, private networking, or `ablo dev --local` with a signed Data Source reverse channel."
440
+ "The database host did not resolve exclusively to an allowed address for this route. Use a publicly resolvable direct endpoint, or configure the matching PrivateLink, peering, or VPN route. Localhost has its own `database_loopback_requires_connector` workflow."
441
+ ),
442
+ database_loopback_requires_connector: wire(
443
+ 'validation',
444
+ 400,
445
+ false,
446
+ 'Ablo Cloud cannot open a direct PostgreSQL connection to localhost on your machine. For localhost-first development, run `ablo migrate` once and keep `ablo dev --local` running; use a network-reachable direct route only when Ablo must observe arbitrary SQL writes through WAL.'
383
447
  ),
384
448
  connected_database_unreachable: wire(
385
449
  'tenant',
@@ -436,7 +500,7 @@ export const ERROR_CODES = {
436
500
  'claim',
437
501
  409,
438
502
  false,
439
- 'This write carried a fencing token below the row’s current high-water: a later holder claimed the row, wrote, and moved on while this claim was lapsed, so applying the write would silently overwrite their work. The claim is gone — re-claim the row and retry from the current state.'
503
+ 'This claim is no longer current because another participant completed newer work on the row. Re-claim the row and retry from the fresh state.'
440
504
  ),
441
505
  entity_claimed: wire(
442
506
  'claim',
@@ -492,7 +556,7 @@ export const ERROR_CODES = {
492
556
  ),
493
557
  model_claim_not_configured: client(
494
558
  'claim',
495
- 'Claiming requires the collaboration runtime, which the standard Ablo({ schema, apiKey }) client wires up for every model automatically — there is no per-model claim configuration to add. This appears only when a model proxy is constructed directly without that runtime (an internal/advanced path).'
559
+ 'Claiming is unavailable on this model client. Construct it through the standard Ablo({ schema, apiKey }) client and retry.'
496
560
  ),
497
561
  model_join_not_configured: client(
498
562
  'claim',
@@ -741,7 +805,9 @@ export const ERROR_CODES = {
741
805
  'not_found',
742
806
  404,
743
807
  false,
744
- 'This branch is not connected to your database yet. Run `ablo connect` for a cloud-reachable direct Postgres endpoint, or `ablo dev --local` to register and serve a localhost Data Source, then retry.'
808
+ 'This branch is not connected to your database yet. Run `ablo connect` for a cloud-reachable direct Postgres endpoint, or `ablo dev --local` to register and serve a localhost Data Source, then retry.',
809
+ undefined,
810
+ true,
745
811
  ),
746
812
  source_connector_no_source_registered: wire(
747
813
  'not_found',
@@ -1187,6 +1253,30 @@ export const ERROR_CODES = {
1187
1253
  ),
1188
1254
 
1189
1255
  // ── server (5xx) ───────────────────────────────────────────────────
1256
+ session_check_failed: wire(
1257
+ 'server',
1258
+ 503,
1259
+ true,
1260
+ 'Ablo could not verify the current login session. Retry shortly.'
1261
+ ),
1262
+ mint_failed: wire(
1263
+ 'server',
1264
+ 502,
1265
+ true,
1266
+ 'Ablo could not mint the requested runtime credential. Retry shortly.'
1267
+ ),
1268
+ server_side_only: wire(
1269
+ 'permission', 403, false, 'This endpoint is available only to trusted server-side callers.'
1270
+ ),
1271
+ unknown_auth_host: wire(
1272
+ 'auth', 400, false, 'The authentication host is not recognized.'
1273
+ ),
1274
+ no_active_organization: wire(
1275
+ 'tenant', 400, false, 'Select an active organization before continuing.'
1276
+ ),
1277
+ data_session_unsupported: wire(
1278
+ 'validation', 400, false, 'This sign-in host cannot create the requested data session.'
1279
+ ),
1190
1280
  internal_error: wire(
1191
1281
  'server',
1192
1282
  500,
@@ -1439,7 +1529,9 @@ export const ERROR_CODES = {
1439
1529
  'permission',
1440
1530
  403,
1441
1531
  false,
1442
- 'Schema registration could not create tables in the target database: the engine is not permitted to run DDL there.'
1532
+ 'Schema registration could not create tables in the target database: the engine is not permitted to run DDL there.',
1533
+ undefined,
1534
+ true,
1443
1535
  ),
1444
1536
  model_query_failed: wire(
1445
1537
  'validation',
@@ -1633,6 +1725,10 @@ export const ERROR_CODES = {
1633
1725
  ),
1634
1726
  request_too_large: wire('validation', 413, false, 'The request body exceeds the maximum size.'),
1635
1727
  invalid_schema: wire('validation', 400, false, 'The submitted schema could not be parsed.'),
1728
+ operational_warning: client(
1729
+ 'server',
1730
+ 'Ablo handled an operational degradation that remains searchable for diagnosis.'
1731
+ ),
1636
1732
  incompatible_change: wire(
1637
1733
  'conflict',
1638
1734
  409,
@@ -0,0 +1,121 @@
1
+ import { z } from 'zod';
2
+
3
+ export const errorObservationSeveritySchema = z.enum([
4
+ 'debug', 'info', 'warning', 'error', 'fatal',
5
+ ]);
6
+
7
+ export const errorObservationChannelSchema = z.enum([
8
+ 'http', 'websocket', 'connector', 'background', 'process', 'cli', 'browser',
9
+ ]);
10
+
11
+ const TRUNCATION_MARKER = '…[truncated]';
12
+
13
+ /**
14
+ * Keep an observation field inside its wire bound without dropping the event.
15
+ * This lives beside the schema so producers cannot accidentally enforce a
16
+ * different limit. Redaction still happens before parsing at each boundary.
17
+ */
18
+ export function boundObservationString(value: string, maxLength: number): string {
19
+ if (value.length <= maxLength) return value;
20
+ return `${value.slice(0, maxLength - TRUNCATION_MARKER.length)}${TRUNCATION_MARKER}`;
21
+ }
22
+
23
+ const boundedString = (maxLength: number) =>
24
+ z.preprocess(
25
+ (value) => typeof value === 'string' ? boundObservationString(value, maxLength) : value,
26
+ z.string().max(maxLength),
27
+ );
28
+
29
+ const boundedCauseSchema = z.object({
30
+ type: boundedString(200),
31
+ message: boundedString(2_000),
32
+ });
33
+
34
+ /** Runtime-neutral private event vocabulary shared by every error boundary. */
35
+ export const errorObservationSchema = z.object({
36
+ eventId: boundedString(200).pipe(z.string().min(1)),
37
+ occurredAt: z.iso.datetime(),
38
+ service: boundedString(100).pipe(z.string().min(1)),
39
+ stage: boundedString(100).pipe(z.string().min(1)),
40
+ release: boundedString(200).optional(),
41
+ severity: errorObservationSeveritySchema,
42
+ channel: errorObservationChannelSchema,
43
+ scope: boundedString(200).pipe(z.string().min(1)),
44
+ operation: boundedString(300).pipe(z.string().min(1)),
45
+ errorCode: boundedString(200).pipe(z.string().min(1)),
46
+ errorType: boundedString(200).pipe(z.string().min(1)),
47
+ category: boundedString(100).pipe(z.string().min(1)),
48
+ retryable: z.boolean(),
49
+ /** Whether application code converted the failure into an expected outcome. */
50
+ handled: z.boolean(),
51
+ publicMessage: boundedString(2_000),
52
+ internalMessage: boundedString(4_000).optional(),
53
+ stack: boundedString(20_000).optional(),
54
+ httpStatus: z.number().int().min(100).max(599).optional(),
55
+ requestId: boundedString(200).optional(),
56
+ traceId: boundedString(200).optional(),
57
+ spanId: boundedString(200).optional(),
58
+ organizationId: boundedString(200).optional(),
59
+ projectId: boundedString(200).optional(),
60
+ branchId: boundedString(200).optional(),
61
+ keyKind: boundedString(100).optional(),
62
+ storageKind: boundedString(100).optional(),
63
+ storageTransport: boundedString(100).optional(),
64
+ dataSourceId: boundedString(200).optional(),
65
+ connectorState: boundedString(100).optional(),
66
+ model: boundedString(200).optional(),
67
+ cause: z.array(boundedCauseSchema).max(5).optional(),
68
+ diagnosticContext: z.record(z.string(), z.unknown()).optional(),
69
+ });
70
+
71
+ export type ErrorObservation = Readonly<z.infer<typeof errorObservationSchema>>;
72
+ export type ErrorObservationChannel = z.infer<typeof errorObservationChannelSchema>;
73
+ export type ErrorObservationSeverity = z.infer<typeof errorObservationSeveritySchema>;
74
+
75
+ const SENSITIVE_KEYS = [
76
+ 'password', 'secret', 'token', 'apikey', 'authorization', 'cookie',
77
+ 'databaseurl', 'connectionstring', 'ek', 'sk', 'rk', 'body', 'payload', 'sql', 'query',
78
+ ];
79
+ const MASK = '[redacted]';
80
+ const SECRET_PATTERNS = [
81
+ /postgres(?:ql)?:\/\/[^\s"']+/gi,
82
+ /\b[a-z]{2,4}_[A-Za-z0-9]{12,}\b/g,
83
+ ];
84
+
85
+ export function redactObservationString(value: string): string {
86
+ return SECRET_PATTERNS.reduce((current, pattern) => current.replace(pattern, MASK), value);
87
+ }
88
+
89
+ function isSensitiveObservationKey(key: string): boolean {
90
+ const normalized = key.toLowerCase().replace(/[^a-z]/g, '');
91
+ return SENSITIVE_KEYS.some(
92
+ (sensitive) => normalized === sensitive || (sensitive.length > 3 && normalized.includes(sensitive)),
93
+ );
94
+ }
95
+
96
+ /** Runtime-neutral, circular-safe sanitizer used before any observation sink. */
97
+ export function sanitizeObservationValue(
98
+ value: unknown,
99
+ depth = 0,
100
+ seen = new WeakSet(),
101
+ ): unknown {
102
+ if (depth > 6) return '[truncated]';
103
+ if (typeof value === 'string') return redactObservationString(value);
104
+ if (value && typeof value === 'object') {
105
+ if (seen.has(value)) return '[circular]';
106
+ seen.add(value);
107
+ }
108
+ if (Array.isArray(value)) {
109
+ return value.map((item) => sanitizeObservationValue(item, depth + 1, seen));
110
+ }
111
+ if (value && typeof value === 'object') {
112
+ const sanitized: Record<string, unknown> = {};
113
+ for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
114
+ sanitized[key] = isSensitiveObservationKey(key)
115
+ ? MASK
116
+ : sanitizeObservationValue(item, depth + 1, seen);
117
+ }
118
+ return sanitized;
119
+ }
120
+ return value;
121
+ }
package/src/errors.ts CHANGED
@@ -64,6 +64,8 @@ export class AbloError extends Error {
64
64
  * `x-request-id` header or a live commit's rejection frame. Include it in
65
65
  * support requests. */
66
66
  readonly requestId?: string;
67
+ /** Opaque id of the exact private observation associated with this error. */
68
+ readonly eventId?: string;
67
69
  /** The specific input that caused the error, as a model or field path such as
68
70
  * `'dataroomMember.grants.subject'`, so tooling can point at the exact
69
71
  * offending value. */
@@ -96,6 +98,7 @@ export class AbloError extends Error {
96
98
  if (options?.requestId !== undefined) this.requestId = options.requestId;
97
99
  if (options?.param !== undefined) this.param = options.param;
98
100
  if (options?.details !== undefined) this.details = options.details;
101
+ if (typeof options?.details?.event_id === 'string') this.eventId = options.details.event_id;
99
102
  const docUrl = options?.docUrl ?? (options?.code ? docUrlForCode(options.code) : undefined);
100
103
  if (docUrl !== undefined) this.docUrl = docUrl;
101
104
  if (options?.cause !== undefined) {
@@ -142,7 +145,8 @@ export class AbloError extends Error {
142
145
  const code = this.code ? ` [${this.code}]` : '';
143
146
  const docs = this.docUrl ? ` (see ${this.docUrl})` : '';
144
147
  const req = this.requestId ? ` [request_id: ${this.requestId}]` : '';
145
- return `${this.name}${code}: ${this.message}${docs}${req}`;
148
+ const event = this.eventId ? ` [event_id: ${this.eventId}]` : '';
149
+ return `${this.name}${code}: ${this.message}${docs}${req}${event}`;
146
150
  }
147
151
  }
148
152
 
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The Ablo settlement core (ADR 0013).
2
+ * The Ablo confirmation core (ADR 0013).
3
3
  *
4
4
  * The transaction layer that the reactive client is a consumer of, not the
5
5
  * other way round. The root barrel is deliberate: it exports the seam —
@@ -31,8 +31,15 @@ export { Ablo } from './ablo.js';
31
31
  export type {
32
32
  AbloHttpClient,
33
33
  AbloHttpClientOptions,
34
+ CapturedRow,
34
35
  HttpModelClient,
36
+ HttpModelMutationParams,
35
37
  } from './transport/httpClient.js';
38
+ export type {
39
+ FunctionalUpdateOptions,
40
+ /** @deprecated Use `FunctionalUpdateOptions`. */
41
+ ContentionOptions,
42
+ } from './resources/functionalUpdate.js';
36
43
 
37
44
  // The types the seam's signatures reference.
38
45
  export type {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Package-private ReadSet integration seam used by the reactive materialiser.
3
+ * Application code declares dependencies through `reads: [returnedRow]`.
4
+ */
5
+ export {
6
+ abortReadSetCommit,
7
+ capturePointRead,
8
+ createReadSetContext,
9
+ commitRecordIdentity,
10
+ consumeReadSet,
11
+ prepareReadSet,
12
+ publishCommitRecord,
13
+ } from '../readSetContext.js';
14
+ export { recordWebSocketCommitReceipt } from '../commitRecordRuntime.js';
15
+ export type {
16
+ PreparedReadSet,
17
+ ReadSetContext,
18
+ } from '../readSetContext.js';
package/src/logger.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A contract with no framework and no local state: credential exchange, commit,
5
5
  * and claim all need to log with no UI and no offline store present, so the port
6
- * belongs to the settlement core (ADR 0016). The consumer supplies the
6
+ * belongs to the confirmation core (ADR 0016). The consumer supplies the
7
7
  * implementation; the SDK ships a no-op default.
8
8
  */
9
9
  export interface Logger {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The observability the settlement core reports on its own behalf.
2
+ * The observability the confirmation core reports on its own behalf.
3
3
  *
4
4
  * Coordination outcomes — a claim changing state, a stale-write collision that
5
5
  * notified instead of aborting — happen with no UI and no local store anywhere,
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Client-local opaque evidence for rows returned by authoritative reads.
3
+ *
4
+ * The name is retained internally to avoid churn across the two transports,
5
+ * but this is not an execution context: it has no async carrier and no ambient
6
+ * scope. Evidence is keyed only by the exact object returned to the caller.
7
+ */
8
+ import {
9
+ readDependencySchema,
10
+ type CommitReadSetEntry,
11
+ type OnStaleMode,
12
+ type ReadDependency,
13
+ } from './coordination/schema.js';
14
+ import { AbloValidationError } from './errors.js';
15
+ import type { CommitRecord } from './wire/commit.js';
16
+
17
+ type ClientIdentity = object;
18
+
19
+ /** @internal One exact returned-row assertion. */
20
+ export interface CapturedReadEvidence {
21
+ readonly client: ClientIdentity;
22
+ readonly entry: CommitReadSetEntry;
23
+ readonly row: object;
24
+ }
25
+
26
+ /** @internal Per-client registry. Weak keys do not retain application rows. */
27
+ export interface ReadRegistry {
28
+ readonly byRow: WeakMap<object, CapturedReadEvidence>;
29
+ readonly commitRecords: Map<string, CommitRecord>;
30
+ onCommitRecord?: (record: CommitRecord) => void | Promise<void>;
31
+ }
32
+
33
+ /** @internal Client-local evidence registry handle. */
34
+ export interface ReadSetContext {
35
+ getStore(): ReadRegistry;
36
+ }
37
+
38
+ export interface PreparedReadSet {
39
+ readonly readAt?: number;
40
+ readonly onStale?: OnStaleMode;
41
+ readonly reads?: readonly ReadDependency[] | null;
42
+ readonly consumed: readonly CapturedReadEvidence[];
43
+ readonly automaticCommit: false;
44
+ readonly idempotencyKey?: string;
45
+ }
46
+
47
+ /** @internal Creates one isolated evidence registry for one Ablo client. */
48
+ export function createReadSetContext(options?: {
49
+ readonly onCommitRecord?: (record: CommitRecord) => void | Promise<void>;
50
+ }): ReadSetContext {
51
+ const registry: ReadRegistry = {
52
+ byRow: new WeakMap(),
53
+ commitRecords: new Map(),
54
+ ...(options?.onCommitRecord ? { onCommitRecord: options.onCommitRecord } : {}),
55
+ };
56
+ return { getStore: () => registry };
57
+ }
58
+
59
+ /** @internal Retains and reports one immutable logical commit snapshot. */
60
+ export function publishCommitRecord(
61
+ context: ReadSetContext | undefined,
62
+ record: CommitRecord,
63
+ ): void {
64
+ const registry = context?.getStore();
65
+ if (!registry) return;
66
+ registry.commitRecords.set(record.id, record);
67
+ try {
68
+ const observed = registry.onCommitRecord?.(record);
69
+ if (observed) void Promise.resolve(observed).catch(() => undefined);
70
+ } catch {
71
+ // Observability must never change a commit's outcome.
72
+ }
73
+ }
74
+
75
+ /** @internal Associates evidence with the exact row object returned to a caller. */
76
+ export function capturePointRead(
77
+ context: ReadSetContext | undefined,
78
+ client: ClientIdentity,
79
+ model: string,
80
+ id: string,
81
+ row: unknown,
82
+ readAt: number,
83
+ ): void {
84
+ if (!context || typeof row !== 'object' || row === null) return;
85
+ const rowObject = row as object;
86
+ context.getStore().byRow.set(rowObject, {
87
+ client,
88
+ row: rowObject,
89
+ entry: {
90
+ target: { scope: 'row', model, id },
91
+ watermark: readAt,
92
+ lifetime: 'commit',
93
+ onStale: 'reject',
94
+ },
95
+ });
96
+ }
97
+
98
+ /** @internal Resolves only dependencies explicitly supplied through `reads`. */
99
+ export function prepareReadSet(
100
+ context: ReadSetContext | undefined,
101
+ client: ClientIdentity,
102
+ explicitReadAt: number | null | undefined,
103
+ explicitOnStale: OnStaleMode | null | undefined,
104
+ explicitIdempotencyKey: string | null | undefined,
105
+ reads: readonly unknown[] | null | undefined,
106
+ ): PreparedReadSet {
107
+ const registry = context?.getStore();
108
+ const consumed: CapturedReadEvidence[] = [];
109
+ const resolvedReads: ReadDependency[] = [];
110
+
111
+ for (const entry of reads ?? []) {
112
+ const captured =
113
+ registry && typeof entry === 'object' && entry !== null
114
+ ? registry.byRow.get(entry)
115
+ : undefined;
116
+ if (captured?.client === client) {
117
+ const target = captured.entry.target;
118
+ if (target.scope !== 'row') {
119
+ throw new AbloValidationError('Captured read evidence must name a row.', {
120
+ code: 'write_options_invalid', param: 'reads',
121
+ });
122
+ }
123
+ resolvedReads.push({
124
+ model: target.model,
125
+ id: target.id,
126
+ readAt: captured.entry.watermark,
127
+ });
128
+ consumed.push(captured);
129
+ continue;
130
+ }
131
+ const canonical = readDependencySchema.safeParse(entry);
132
+ if (canonical.success) {
133
+ resolvedReads.push(canonical.data);
134
+ continue;
135
+ }
136
+ throw new AbloValidationError(
137
+ 'A row passed through `reads` was not the exact object returned by this Ablo client. Re-read it and pass that object directly.',
138
+ { code: 'write_options_invalid', param: 'reads' },
139
+ );
140
+ }
141
+
142
+ return {
143
+ ...(explicitReadAt != null ? { readAt: explicitReadAt } : {}),
144
+ ...(explicitOnStale != null ? { onStale: explicitOnStale } : {}),
145
+ ...(reads !== undefined ? { reads: reads === null ? null : resolvedReads } : {}),
146
+ consumed,
147
+ automaticCommit: false,
148
+ ...(explicitIdempotencyKey != null ? { idempotencyKey: explicitIdempotencyKey } : {}),
149
+ };
150
+ }
151
+
152
+ /** @internal Commit identity is the explicit request idempotency identity. */
153
+ export function commitRecordIdentity(
154
+ _context: ReadSetContext | undefined,
155
+ attemptId: string,
156
+ ): { readonly id: string } {
157
+ return { id: attemptId };
158
+ }
159
+
160
+ /** @internal Explicit row evidence is reusable; successful writes do not consume it. */
161
+ export function consumeReadSet(..._args: readonly unknown[]): void {}
162
+
163
+ /** @internal There is no ambient automatic-commit reservation to release. */
164
+ export function abortReadSetCommit(..._args: readonly unknown[]): void {}
@@ -28,6 +28,7 @@ import {
28
28
  AbloClaimedError,
29
29
  AbloContentionError,
30
30
  } from '../errors.js';
31
+ import type { ReadDependency } from '../coordination/schema.js';
31
32
 
32
33
  /**
33
34
  * The functional form of an update: given the freshly-read row, return the
@@ -38,8 +39,8 @@ export type ModelUpdater<T> = (
38
39
  current: T,
39
40
  ) => Partial<T> | null | undefined | Promise<Partial<T> | null | undefined>;
40
41
 
41
- /** Tuning for the functional update's internal reconcile loop. */
42
- export interface ContentionOptions {
42
+ /** Options for the functional `update(id, reducer, options)` form. */
43
+ export interface FunctionalUpdateOptions<Dependency = ReadDependency> {
43
44
  /**
44
45
  * Max reconcile rounds under contention before throwing
45
46
  * {@link AbloContentionError}. Each round re-reads the latest row and re-runs
@@ -48,8 +49,19 @@ export interface ContentionOptions {
48
49
  readonly retries?: number;
49
50
  /** Abort the reconcile loop (e.g. the request was cancelled). */
50
51
  readonly signal?: AbortSignal;
52
+ /**
53
+ * Cross-target state this reducer decision depends on. Captured point rows
54
+ * are resolved by the public client; canonical dependencies remain the
55
+ * low-level escape hatch. Dependencies survive reconcilable CAS failures and
56
+ * are consumed only with the successful attempt.
57
+ */
58
+ readonly reads?: readonly Dependency[] | null;
51
59
  }
52
60
 
61
+ /** @deprecated Use {@link FunctionalUpdateOptions}. */
62
+ export type ContentionOptions<Dependency = ReadDependency> =
63
+ FunctionalUpdateOptions<Dependency>;
64
+
53
65
  /** Reconcile rounds before a hot row is declared permanently contended. */
54
66
  export const DEFAULT_CONTENTION_RETRIES = 16;
55
67
 
@@ -104,9 +116,9 @@ export interface ReconcileTransport<T, R> {
104
116
  * both transports so the guarantee is provably identical. Returns the write's
105
117
  * result, or `undefined` when the updater opted out of writing.
106
118
  */
107
- export async function reconcileFunctionalUpdate<T, R>(
119
+ export async function reconcileFunctionalUpdate<T, R, Read = ReadDependency>(
108
120
  updater: ModelUpdater<T>,
109
- options: ContentionOptions | undefined,
121
+ options: FunctionalUpdateOptions<Read> | undefined,
110
122
  transport: ReconcileTransport<T, R>,
111
123
  ): Promise<R | undefined> {
112
124
  const retries = options?.retries ?? DEFAULT_CONTENTION_RETRIES;