@abloatai/transaction 0.51.0 → 0.53.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 (216) hide show
  1. package/dist/ablo.d.ts +1 -1
  2. package/dist/ablo.js +1 -1
  3. package/dist/ai-sdk/updateTool.d.ts +1 -1
  4. package/dist/ai-sdk/updateTool.js +1 -1
  5. package/dist/auth/apiKey.d.ts +0 -6
  6. package/dist/auth/apiKey.d.ts.map +1 -1
  7. package/dist/auth/apiKey.js +13 -60
  8. package/dist/auth/apiKey.js.map +1 -1
  9. package/dist/auth/baseUrl.d.ts +38 -0
  10. package/dist/auth/baseUrl.d.ts.map +1 -0
  11. package/dist/auth/baseUrl.js +126 -0
  12. package/dist/auth/baseUrl.js.map +1 -0
  13. package/dist/auth/capability.d.ts +2 -2
  14. package/dist/auth/capability.js +1 -1
  15. package/dist/auth/index.d.ts +15 -2
  16. package/dist/auth/index.d.ts.map +1 -1
  17. package/dist/auth/index.js +35 -21
  18. package/dist/auth/index.js.map +1 -1
  19. package/dist/auth/schemas.d.ts +24 -1
  20. package/dist/auth/schemas.d.ts.map +1 -1
  21. package/dist/auth/schemas.js +8 -2
  22. package/dist/auth/schemas.js.map +1 -1
  23. package/dist/auth/sessionIssuanceProtocol.d.ts +41 -0
  24. package/dist/auth/sessionIssuanceProtocol.d.ts.map +1 -0
  25. package/dist/auth/sessionIssuanceProtocol.js +48 -0
  26. package/dist/auth/sessionIssuanceProtocol.js.map +1 -0
  27. package/dist/auth/sessionMint.js +2 -2
  28. package/dist/branches.d.ts +34 -33
  29. package/dist/branches.d.ts.map +1 -1
  30. package/dist/branches.js +2 -4
  31. package/dist/branches.js.map +1 -1
  32. package/dist/coordination/schema.d.ts +3 -2
  33. package/dist/coordination/schema.d.ts.map +1 -1
  34. package/dist/coordination/schema.js +7 -2
  35. package/dist/coordination/schema.js.map +1 -1
  36. package/dist/errorCodes.d.ts +7 -3
  37. package/dist/errorCodes.d.ts.map +1 -1
  38. package/dist/errorCodes.js +8 -4
  39. package/dist/errorCodes.js.map +1 -1
  40. package/dist/errors.d.ts +1 -1
  41. package/dist/errors.js +1 -1
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/resources/httpResources.d.ts +35 -2
  46. package/dist/resources/httpResources.d.ts.map +1 -1
  47. package/dist/resources/httpResources.js +10 -1
  48. package/dist/resources/httpResources.js.map +1 -1
  49. package/dist/resources/modelOperations.d.ts +18 -6
  50. package/dist/resources/modelOperations.d.ts.map +1 -1
  51. package/dist/resources/modelOperations.js.map +1 -1
  52. package/dist/resources/where.d.ts +67 -2
  53. package/dist/resources/where.d.ts.map +1 -1
  54. package/dist/resources/where.js +73 -0
  55. package/dist/resources/where.js.map +1 -1
  56. package/dist/schema/ddl.d.ts.map +1 -1
  57. package/dist/schema/ddl.js +9 -12
  58. package/dist/schema/ddl.js.map +1 -1
  59. package/dist/schema/field.d.ts +3 -3
  60. package/dist/schema/field.js +2 -2
  61. package/dist/schema/fieldRef.d.ts +1 -1
  62. package/dist/schema/generate.d.ts +2 -3
  63. package/dist/schema/generate.d.ts.map +1 -1
  64. package/dist/schema/generate.js +2 -8
  65. package/dist/schema/generate.js.map +1 -1
  66. package/dist/schema/index.d.ts +2 -2
  67. package/dist/schema/index.js +3 -3
  68. package/dist/schema/model.d.ts +7 -7
  69. package/dist/schema/model.js +2 -2
  70. package/dist/schema/openapi.d.ts.map +1 -1
  71. package/dist/schema/openapi.js +63 -21
  72. package/dist/schema/openapi.js.map +1 -1
  73. package/dist/schema/queries.d.ts +1 -1
  74. package/dist/schema/queries.js +1 -1
  75. package/dist/schema/relation.d.ts +8 -8
  76. package/dist/schema/relation.js +5 -5
  77. package/dist/schema/schema.d.ts +24 -37
  78. package/dist/schema/schema.d.ts.map +1 -1
  79. package/dist/schema/schema.js +15 -28
  80. package/dist/schema/schema.js.map +1 -1
  81. package/dist/schema/sugar.d.ts +7 -7
  82. package/dist/schema/sugar.js +5 -5
  83. package/dist/server/adapter.d.ts +2 -0
  84. package/dist/server/adapter.d.ts.map +1 -1
  85. package/dist/server/commit.d.ts +5 -1
  86. package/dist/server/commit.d.ts.map +1 -1
  87. package/dist/server/index.d.ts +3 -2
  88. package/dist/server/index.d.ts.map +1 -1
  89. package/dist/server/index.js.map +1 -1
  90. package/dist/server/readConfig.d.ts +109 -57
  91. package/dist/server/readConfig.d.ts.map +1 -1
  92. package/dist/server/readConfig.js +19 -5
  93. package/dist/server/readConfig.js.map +1 -1
  94. package/dist/source/adapters/drizzle.d.ts.map +1 -1
  95. package/dist/source/adapters/drizzle.js +5 -0
  96. package/dist/source/adapters/drizzle.js.map +1 -1
  97. package/dist/source/adapters/kysely.d.ts.map +1 -1
  98. package/dist/source/adapters/kysely.js +33 -8
  99. package/dist/source/adapters/kysely.js.map +1 -1
  100. package/dist/source/adapters/kyselyMutationCore.d.ts +1 -1
  101. package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
  102. package/dist/source/adapters/kyselyMutationCore.js +58 -20
  103. package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
  104. package/dist/source/adapters/memory.d.ts.map +1 -1
  105. package/dist/source/adapters/memory.js +5 -0
  106. package/dist/source/adapters/memory.js.map +1 -1
  107. package/dist/source/adapters/prisma.d.ts.map +1 -1
  108. package/dist/source/adapters/prisma.js +6 -1
  109. package/dist/source/adapters/prisma.js.map +1 -1
  110. package/dist/source/conformance.js +21 -21
  111. package/dist/source/contract.d.ts +17 -0
  112. package/dist/source/contract.d.ts.map +1 -1
  113. package/dist/source/contract.js +31 -0
  114. package/dist/source/contract.js.map +1 -1
  115. package/dist/source/index.d.ts +1 -1
  116. package/dist/source/index.d.ts.map +1 -1
  117. package/dist/source/index.js +1 -1
  118. package/dist/source/index.js.map +1 -1
  119. package/dist/source/types.d.ts +1 -0
  120. package/dist/source/types.d.ts.map +1 -1
  121. package/dist/source/types.js.map +1 -1
  122. package/dist/transactionLayer.d.ts +3 -3
  123. package/dist/transactionLayer.js +3 -3
  124. package/dist/transport/httpClient.d.ts +13 -4
  125. package/dist/transport/httpClient.d.ts.map +1 -1
  126. package/dist/transport/httpClient.js +4 -2
  127. package/dist/transport/httpClient.js.map +1 -1
  128. package/dist/transport/httpTransport.d.ts +1 -0
  129. package/dist/transport/httpTransport.d.ts.map +1 -1
  130. package/dist/transport/httpTransport.js +79 -11
  131. package/dist/transport/httpTransport.js.map +1 -1
  132. package/dist/transport/wsFrameHandlers.d.ts.map +1 -1
  133. package/dist/transport/wsFrameHandlers.js +3 -0
  134. package/dist/transport/wsFrameHandlers.js.map +1 -1
  135. package/dist/types/streams.d.ts +2 -2
  136. package/dist/wire/accountResponses.d.ts +18 -13
  137. package/dist/wire/accountResponses.d.ts.map +1 -1
  138. package/dist/wire/accountResponses.js +5 -11
  139. package/dist/wire/accountResponses.js.map +1 -1
  140. package/dist/wire/claims.d.ts +1 -0
  141. package/dist/wire/claims.d.ts.map +1 -1
  142. package/dist/wire/claims.js +7 -1
  143. package/dist/wire/claims.js.map +1 -1
  144. package/dist/wire/commit.d.ts +144 -0
  145. package/dist/wire/commit.d.ts.map +1 -1
  146. package/dist/wire/commit.js +22 -1
  147. package/dist/wire/commit.js.map +1 -1
  148. package/dist/wire/frames.d.ts +4 -0
  149. package/dist/wire/frames.d.ts.map +1 -1
  150. package/dist/wire/inboundFrames.d.ts +22 -0
  151. package/dist/wire/inboundFrames.d.ts.map +1 -1
  152. package/dist/wire/index.d.ts +3 -3
  153. package/dist/wire/index.d.ts.map +1 -1
  154. package/dist/wire/index.js +2 -2
  155. package/dist/wire/index.js.map +1 -1
  156. package/dist/wire/listEnvelope.d.ts +20 -0
  157. package/dist/wire/listEnvelope.d.ts.map +1 -1
  158. package/dist/wire/listEnvelope.js +20 -0
  159. package/dist/wire/listEnvelope.js.map +1 -1
  160. package/dist/wire/protocol.d.ts +1 -1
  161. package/dist/wire/protocol.js +1 -1
  162. package/package.json +1 -1
  163. package/src/ablo.ts +1 -1
  164. package/src/ai-sdk/updateTool.ts +1 -1
  165. package/src/auth/apiKey.ts +13 -61
  166. package/src/auth/baseUrl.ts +144 -0
  167. package/src/auth/capability.ts +3 -3
  168. package/src/auth/index.ts +62 -21
  169. package/src/auth/schemas.ts +16 -2
  170. package/src/auth/sessionIssuanceProtocol.ts +57 -0
  171. package/src/auth/sessionMint.ts +2 -2
  172. package/src/branches.ts +2 -4
  173. package/src/coordination/schema.ts +7 -2
  174. package/src/errorCodes.ts +29 -5
  175. package/src/errors.ts +1 -1
  176. package/src/index.ts +2 -0
  177. package/src/resources/httpResources.ts +45 -2
  178. package/src/resources/modelOperations.ts +18 -6
  179. package/src/resources/where.ts +70 -4
  180. package/src/schema/ddl.ts +9 -13
  181. package/src/schema/field.ts +3 -3
  182. package/src/schema/fieldRef.ts +1 -1
  183. package/src/schema/generate.ts +2 -8
  184. package/src/schema/index.ts +3 -3
  185. package/src/schema/model.ts +7 -7
  186. package/src/schema/openapi.ts +64 -21
  187. package/src/schema/queries.ts +1 -1
  188. package/src/schema/relation.ts +8 -8
  189. package/src/schema/residency.ts +0 -1
  190. package/src/schema/schema.ts +33 -58
  191. package/src/schema/sugar.ts +7 -7
  192. package/src/server/adapter.ts +2 -0
  193. package/src/server/commit.ts +5 -1
  194. package/src/server/index.ts +8 -2
  195. package/src/server/readConfig.ts +111 -57
  196. package/src/source/adapters/drizzle.ts +5 -0
  197. package/src/source/adapters/kysely.ts +48 -10
  198. package/src/source/adapters/kyselyMutationCore.ts +69 -22
  199. package/src/source/adapters/memory.ts +5 -0
  200. package/src/source/adapters/prisma.ts +6 -1
  201. package/src/source/conformance.ts +21 -21
  202. package/src/source/contract.ts +34 -0
  203. package/src/source/index.ts +2 -0
  204. package/src/source/types.ts +1 -0
  205. package/src/transactionLayer.ts +3 -3
  206. package/src/transport/httpClient.ts +23 -6
  207. package/src/transport/httpTransport.ts +88 -11
  208. package/src/transport/wsFrameHandlers.ts +3 -0
  209. package/src/types/global.ts +1 -1
  210. package/src/types/streams.ts +2 -2
  211. package/src/wire/accountResponses.ts +5 -11
  212. package/src/wire/claims.ts +7 -1
  213. package/src/wire/commit.ts +31 -1
  214. package/src/wire/index.ts +10 -1
  215. package/src/wire/listEnvelope.ts +23 -0
  216. package/src/wire/protocol.ts +1 -1
package/src/auth/index.ts CHANGED
@@ -12,6 +12,8 @@
12
12
  import {
13
13
  type EphemeralKeyResponse,
14
14
  type IdentityResolveResponse,
15
+ type SessionCredentialMetadata,
16
+ type SessionCredentialRevocationRequest,
15
17
  parseCapabilityMintResponse,
16
18
  parseEphemeralKeyResponse,
17
19
  parseIdentityResolveResponse,
@@ -27,7 +29,17 @@ export {
27
29
  ABLO_HOSTED_API_DOMAIN,
28
30
  ABLO_HOSTED_HTTP_BASE_URL,
29
31
  } from './hostedEndpoints.js';
30
- export { normalizeAbloHostedBaseUrl } from './apiKey.js';
32
+ export { normalizeAbloBaseUrl } from './baseUrl.js';
33
+ // A re-export does not bind the name in this module; the alias below needs it.
34
+ import { normalizeAbloBaseUrl } from './baseUrl.js';
35
+
36
+ /**
37
+ * @deprecated Renamed to {@link normalizeAbloBaseUrl}. Removed in 0.54.0.
38
+ * The "hosted" spelling named a rewrite of retired hosted hostnames that no
39
+ * longer exists; both spellings resolve through the one function, so a caller
40
+ * on the old name gets the same refusals of unsafe credential destinations.
41
+ */
42
+ export const normalizeAbloHostedBaseUrl = normalizeAbloBaseUrl;
31
43
 
32
44
  /**
33
45
  * @deprecated Use {@link CapabilityMintResponse}. This is a type-only,
@@ -39,6 +51,26 @@ export type CapabilityExchangeResponse = CapabilityMintResponse;
39
51
  export type {
40
52
  EphemeralKeyResponse,
41
53
  IdentityResolveResponse,
54
+ SessionCredentialMetadata,
55
+ SessionCredentialRevocationRequest,
56
+ } from './schemas.js';
57
+ export {
58
+ buildSessionIssuanceMacMessage,
59
+ canonicalizeSessionIssuanceBody,
60
+ opaqueSessionFingerprintSchema,
61
+ sha256HexSchema,
62
+ SESSION_ISSUANCE_HEADERS,
63
+ SESSION_ISSUANCE_MAC_VERSION,
64
+ SESSION_ISSUANCE_PATHS,
65
+ SESSION_ISSUANCE_SLOT_VERSION,
66
+ sessionIssuanceMacHeadersSchema,
67
+ sessionIssuanceMacMessageSchema,
68
+ type SessionIssuanceMacHeaders,
69
+ type SessionIssuanceMacMessage,
70
+ } from './sessionIssuanceProtocol.js';
71
+ export {
72
+ SessionCredentialMetadataSchema,
73
+ SessionCredentialRevocationRequestSchema,
42
74
  } from './schemas.js';
43
75
  export { parseCapabilityMintResponse } from './schemas.js';
44
76
  export {
@@ -99,6 +131,10 @@ export type {
99
131
  // A re-export does not bind the name in this module, and the mint request type
100
132
  // below needs it.
101
133
  import type { CapabilityOperation } from './capability.js';
134
+ import {
135
+ ephemeralKeyRequestSchema,
136
+ type EphemeralKeyRequest,
137
+ } from '../wire/auth.js';
102
138
 
103
139
  export interface ExchangeApiKeyRequest {
104
140
  readonly apiKey: string;
@@ -265,6 +301,30 @@ export type MintUserSessionRequest = MintUserSessionBase & {
265
301
  readonly controlPlaneOnly?: true;
266
302
  };
267
303
 
304
+ /** Canonical wire projection shared by the SDK and first-party issuer. */
305
+ export function buildMintUserSessionRequest(
306
+ options: MintUserSessionRequest,
307
+ ): EphemeralKeyRequest {
308
+ return ephemeralKeyRequestSchema.parse({
309
+ user: { id: options.userId },
310
+ ...(options.organizationId ? { organizationId: options.organizationId } : {}),
311
+ ...(options.schemaProject
312
+ ? {
313
+ schemaProjectId: options.schemaProject.projectId,
314
+ schemaOwnerOrgId: options.schemaProject.organizationId,
315
+ }
316
+ : {}),
317
+ ...(options.syncGroups ? { syncGroups: options.syncGroups } : {}),
318
+ ...(options.controlPlaneOnly
319
+ ? { controlPlaneOnly: true }
320
+ : options.operations
321
+ ? { operations: options.operations }
322
+ : { activeSchemaOperations: options.activeSchemaOperations }),
323
+ ttlSeconds: options.ttlSeconds,
324
+ ...(options.label ? { label: options.label } : {}),
325
+ });
326
+ }
327
+
268
328
  /**
269
329
  * Mints an end-user session key (an `ek_` key) by calling
270
330
  * `POST /v1/ephemeral_keys`, using your secret key as authorization. Your
@@ -322,26 +382,7 @@ export async function mintUserSessionKey(
322
382
  'Content-Type': 'application/json',
323
383
  Authorization: `Bearer ${options.apiKey}`,
324
384
  },
325
- body: JSON.stringify({
326
- user: { id: options.userId },
327
- ...(options.organizationId ? { organizationId: options.organizationId } : {}),
328
- // The public option is project-centric; map it to the flat wire keys the
329
- // endpoint expects.
330
- ...(options.schemaProject
331
- ? {
332
- schemaProjectId: options.schemaProject.projectId,
333
- schemaOwnerOrgId: options.schemaProject.organizationId,
334
- }
335
- : {}),
336
- ...(options.syncGroups ? { syncGroups: options.syncGroups } : {}),
337
- ...(options.controlPlaneOnly
338
- ? { controlPlaneOnly: true }
339
- : options.operations
340
- ? { operations: options.operations }
341
- : { activeSchemaOperations: options.activeSchemaOperations }),
342
- ttlSeconds: options.ttlSeconds,
343
- ...(options.label ? { label: options.label } : {}),
344
- }),
385
+ body: JSON.stringify(buildMintUserSessionRequest(options)),
345
386
  signal: controller.signal,
346
387
  });
347
388
  } catch (err) {
@@ -9,6 +9,7 @@ import {
9
9
  import { AbloAuthenticationError } from '../errors.js';
10
10
  import { authTokenSchema } from './token.js';
11
11
  import { deliveryPartitionRouteSchema } from './deliveryPartition.js';
12
+ import { opaqueSessionFingerprintSchema } from './sessionIssuanceProtocol.js';
12
13
 
13
14
  // Not a second enum. The auth responses carry the same participant vocabulary
14
15
  // the coordination plane parses, so they validate against the same schema — a
@@ -40,10 +41,9 @@ export type IdentityResolveResponse = z.infer<typeof IdentityResolveResponseSche
40
41
  * block. It still echoes the effective operations stored on the key so the
41
42
  * client reports enforced authority rather than reconstructing it from input.
42
43
  */
43
- export const EphemeralKeyResponseSchema = z.object({
44
+ export const SessionCredentialMetadataSchema = z.object({
44
45
  object: z.literal('ephemeral_key').optional(),
45
46
  id: z.string().min(1),
46
- token: authTokenSchema,
47
47
  expiresAt: z.string().min(1),
48
48
  organizationId: z.string().min(1),
49
49
  participantId: z.string().min(1),
@@ -67,6 +67,20 @@ export const EphemeralKeyResponseSchema = z.object({
67
67
  },
68
68
  );
69
69
 
70
+ /** Persistable/replayable projection of the canonical mint response. */
71
+ export type SessionCredentialMetadata = z.infer<typeof SessionCredentialMetadataSchema>;
72
+
73
+ export const SessionCredentialRevocationRequestSchema = z.object({
74
+ parentSessionHash: opaqueSessionFingerprintSchema,
75
+ });
76
+ export type SessionCredentialRevocationRequest = z.infer<
77
+ typeof SessionCredentialRevocationRequestSchema
78
+ >;
79
+
80
+ export const EphemeralKeyResponseSchema = SessionCredentialMetadataSchema.safeExtend({
81
+ token: authTokenSchema,
82
+ });
83
+
70
84
  export type EphemeralKeyResponse = z.infer<typeof EphemeralKeyResponseSchema>;
71
85
 
72
86
  function formatIssues(error: z.ZodError): string {
@@ -0,0 +1,57 @@
1
+ import { z } from 'zod';
2
+ import { stableStringify } from '../utils/json.js';
3
+
4
+ /** Bump only with a coordinated MAC-message protocol rollout. */
5
+ export const SESSION_ISSUANCE_MAC_VERSION = 'v1' as const;
6
+ /** Bump whenever canonical mint semantics change incompatibly. */
7
+ export const SESSION_ISSUANCE_SLOT_VERSION = 'v1' as const;
8
+
9
+ export const SESSION_ISSUANCE_PATHS = {
10
+ mint: '/api/v1/ephemeral_keys',
11
+ revoke: '/api/v1/internal/session-credentials/revoke',
12
+ } as const;
13
+
14
+ export const SESSION_ISSUANCE_HEADERS = {
15
+ slotKey: 'ablo-session-slot',
16
+ parentSessionHash: 'ablo-parent-session',
17
+ timestamp: 'ablo-session-issuance-timestamp',
18
+ mac: 'ablo-session-issuance-mac',
19
+ } as const;
20
+
21
+ export const sha256HexSchema = z.string().regex(/^[a-f0-9]{64}$/);
22
+ export const opaqueSessionFingerprintSchema = sha256HexSchema;
23
+
24
+ export const sessionIssuanceMacHeadersSchema = z.object({
25
+ timestamp: z.string().regex(/^\d{13}$/),
26
+ mac: sha256HexSchema,
27
+ });
28
+ export type SessionIssuanceMacHeaders = z.infer<typeof sessionIssuanceMacHeadersSchema>;
29
+
30
+ export const sessionIssuanceMacMessageSchema = z.object({
31
+ method: z.enum(['POST']),
32
+ path: z.enum([SESSION_ISSUANCE_PATHS.mint, SESSION_ISSUANCE_PATHS.revoke]),
33
+ timestamp: z.string().regex(/^\d{13}$/),
34
+ bodySha256: sha256HexSchema,
35
+ slotKey: opaqueSessionFingerprintSchema.optional(),
36
+ parentSessionHash: opaqueSessionFingerprintSchema,
37
+ });
38
+ export type SessionIssuanceMacMessage = z.infer<typeof sessionIssuanceMacMessageSchema>;
39
+
40
+ /** One canonical JSON representation for slot derivation and request MACs. */
41
+ export function canonicalizeSessionIssuanceBody(body: unknown): string {
42
+ return stableStringify(body);
43
+ }
44
+
45
+ /** Canonical, bounded KMS GenerateMac/VerifyMac message (well below 4 KiB). */
46
+ export function buildSessionIssuanceMacMessage(input: SessionIssuanceMacMessage): Uint8Array {
47
+ const value = sessionIssuanceMacMessageSchema.parse(input);
48
+ return new TextEncoder().encode([
49
+ `ablo-session-issuance:${SESSION_ISSUANCE_MAC_VERSION}`,
50
+ value.method,
51
+ value.path,
52
+ value.timestamp,
53
+ value.bodySha256,
54
+ value.slotKey ?? '',
55
+ value.parentSessionHash,
56
+ ].join('\n'));
57
+ }
@@ -13,8 +13,8 @@
13
13
  * participant with `invalid_participant_kind`.
14
14
  * `{ agent }` → POST /v1/capabilities, returning an `rk_` key scoped to exactly
15
15
  * the operations named in `can`. For example
16
- * `can: { tasks: ['update'] }` becomes the wire allowlist entry
17
- * `tasks.update`, which the server matches against the model's
16
+ * `can: { items: ['update'] }` becomes the wire allowlist entry
17
+ * `items.update`, which the server matches against the model's
18
18
  * registered names.
19
19
  *
20
20
  * The caller supplies the already-resolved secret key and base URL in
package/src/branches.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { listEnvelopeSchema } from './wire/listEnvelope.js';
2
3
 
3
4
  /**
4
5
  * A branch handle is for people and URLs. Durable routing always uses the
@@ -47,10 +48,7 @@ export const branchResponseSchema = z.object({
47
48
  });
48
49
  export type BranchResponse = z.infer<typeof branchResponseSchema>;
49
50
 
50
- export const branchListResponseSchema = z.object({
51
- object: z.literal('list'),
52
- data: z.array(branchResponseSchema).readonly(),
53
- });
51
+ export const branchListResponseSchema = listEnvelopeSchema(branchResponseSchema);
54
52
  export type BranchListResponse = z.infer<typeof branchListResponseSchema>;
55
53
 
56
54
  export const createBranchRequestSchema = z.object({
@@ -61,7 +61,7 @@ export function part(name: string): ClaimPart {
61
61
  * The wire spelling of a part name, from whichever spelling the caller used.
62
62
  *
63
63
  * Three, because they are three different promises. A {@link FieldRef} —
64
- * `schema.fields.tasks.status` — is a field the schema declares, so a name that
64
+ * `schema.fields.items.status` — is a field the schema declares, so a name that
65
65
  * does not exist never compiles. `part('B2')` is a name the schema does not
66
66
  * know and says so. A bare string is neither, and survives only because the
67
67
  * erased `SchemaRecord` view and untyped callers still need it.
@@ -330,7 +330,7 @@ const syncIdSchema = z.number().int().nonnegative();
330
330
  *
331
331
  * • `self` — the row that moved IS the group's scope root.
332
332
  * • `parent` — it sits one declared containment edge below the root.
333
- * • `transitive` — the root is ≥2 hops up (`comment → task → project`).
333
+ * • `transitive` — the root is ≥2 hops up (`comment → item → project`).
334
334
  */
335
335
  export const stalePropagationSchema = z
336
336
  .object({
@@ -1417,6 +1417,11 @@ export const commitOperationSchema = writeGuardSchema.extend({
1417
1417
  model: z.string(),
1418
1418
  id: z.string().nullish(),
1419
1419
  input: z.record(z.string(), z.unknown()).nullish(),
1420
+ /** Equality conditions evaluated atomically by the writing database. */
1421
+ where: z
1422
+ .record(z.string().min(1), z.unknown())
1423
+ .refine((value) => Object.keys(value).length > 0, 'where must name at least one field')
1424
+ .nullish(),
1420
1425
  /** Per-op client tx id, echoed on the broadcast delta. */
1421
1426
  transactionId: z.string().nullish(),
1422
1427
  /**
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-04';
42
+ export const ERROR_CONTRACT_VERSION = '2026-08-15';
43
43
 
44
44
  /** A coarse grouping of error codes, used to organize metrics and documentation. */
45
45
  export type ErrorCategory =
@@ -604,6 +604,12 @@ export const ERROR_CODES = {
604
604
  false,
605
605
  'An earlier attempt under this idempotency key was pinned to a different source transport. Restore that route and retry the same key; Ablo will not switch a possibly committed write.'
606
606
  ),
607
+ precondition_failed: wire(
608
+ 'conflict',
609
+ 409,
610
+ false,
611
+ 'A conditional operation did not match the current database row. The complete atomic commit was rejected and no operation in the batch was applied.'
612
+ ),
607
613
  idempotency_key_too_long: wire(
608
614
  'validation',
609
615
  400,
@@ -815,11 +821,11 @@ export const ERROR_CODES = {
815
821
  false,
816
822
  'The branch has no endpoint Data Source for this connector to serve. Run `ablo dev --local` with the current CLI, which registers the connector-only source before opening the socket.'
817
823
  ),
818
- task_id_missing: wire(
824
+ item_id_missing: wire(
819
825
  'server',
820
826
  502,
821
827
  true,
822
- 'The task-create response arrived without a task id, so the result cannot be used. Retry the request.'
828
+ 'The item-create response arrived without an item id, so the result cannot be used. Retry the request.'
823
829
  ),
824
830
 
825
831
  // ── data integrity / database constraints ──────────────────────────
@@ -864,6 +870,12 @@ export const ERROR_CODES = {
864
870
  false,
865
871
  'A structured (JSON) value was written to a column whose database type cannot hold it. Ablo adapts a json field to either a jsonb column (native) or a text column (serialized) — but a scalar column (integer, boolean, uuid, timestamp, …) cannot store a JSON object or array. Use a jsonb or text column for this field. Ablo adapts to your column; it does not alter your schema.'
866
872
  ),
873
+ column_value_out_of_range: wire(
874
+ 'validation',
875
+ 400,
876
+ false,
877
+ 'A stored value is outside the range the field was declared to hold. A number field reads back as a JavaScript number, which represents integers exactly only up to 9,007,199,254,740,991; a bigint column holding more than that would come back rounded. Declare the field as text to read those values digit for digit.'
878
+ ),
867
879
 
868
880
  // ── tenant / unknown model (400) ───────────────────────────────────
869
881
  server_execute_unknown_model: wire(
@@ -918,7 +930,7 @@ export const ERROR_CODES = {
918
930
  ),
919
931
  schema_reserved_field: client(
920
932
  'schema',
921
- 'A model redeclared a reserved base field (id, createdAt, updatedAt, organizationId, createdBy) that the SDK provides automatically.'
933
+ 'A model declared `id`, the one universal field Ablo supplies. Remove it from the declared application fields.'
922
934
  ),
923
935
  schema_grants_shape_invalid: wire(
924
936
  'schema',
@@ -1611,6 +1623,18 @@ export const ERROR_CODES = {
1611
1623
  false,
1612
1624
  'The query contained an invalid identifier.'
1613
1625
  ),
1626
+ query_relation_expansion_too_large: wire(
1627
+ 'validation',
1628
+ 400,
1629
+ false,
1630
+ 'A requested relation expansion exceeds the bounded nested-row budget. Query the related model as its own paginated collection instead.'
1631
+ ),
1632
+ organization_disabled: wire(
1633
+ 'permission',
1634
+ 403,
1635
+ false,
1636
+ 'This organization has been disabled by an operator. Contact support before retrying.'
1637
+ ),
1614
1638
  org_id_required: wire(
1615
1639
  'validation',
1616
1640
  400,
@@ -1647,7 +1671,7 @@ export const ERROR_CODES = {
1647
1671
  false,
1648
1672
  'Uploads are not configured on this deployment: the upload storage bucket and CDN domain are unset.'
1649
1673
  ),
1650
- task_id_required: wire('validation', 400, false, 'A task id is required for this request.'),
1674
+ item_id_required: wire('validation', 400, false, 'An item id is required for this request.'),
1651
1675
  claim_id_required: wire('validation', 400, false, 'A claim id is required for this request.'),
1652
1676
  commit_operation_action_required: wire(
1653
1677
  'validation',
package/src/errors.ts CHANGED
@@ -67,7 +67,7 @@ export class AbloError extends Error {
67
67
  /** Opaque id of the exact private observation associated with this error. */
68
68
  readonly eventId?: string;
69
69
  /** The specific input that caused the error, as a model or field path such as
70
- * `'dataroomMember.grants.subject'`, so tooling can point at the exact
70
+ * `'archiveMember.grants.subject'`, so tooling can point at the exact
71
71
  * offending value. */
72
72
  readonly param?: string;
73
73
  /** A link to the documentation for this error's {@link code}. When not set
package/src/index.ts CHANGED
@@ -35,6 +35,8 @@ export type {
35
35
  HttpModelClient,
36
36
  HttpModelMutationParams,
37
37
  } from './transport/httpClient.js';
38
+ /** What `ablo.<model>.list()` resolves to: the rows, and where the page ends. */
39
+ export type { ModelList } from './resources/httpResources.js';
38
40
  export type {
39
41
  FunctionalUpdateOptions,
40
42
  /** @deprecated Use `FunctionalUpdateOptions`. */
@@ -101,9 +101,51 @@ export interface HttpTransportRead<T = Record<string, unknown>> {
101
101
  /** @internal Exact collection envelope retained until the typed facade captures evidence. */
102
102
  export interface HttpTransportList<T = Record<string, unknown>> {
103
103
  readonly data: readonly T[];
104
+ /**
105
+ * Whether the collection continues past this page. A list read is always a
106
+ * page — the server applies a default size and caps the largest one — so a
107
+ * caller that treats `data` as the whole set is right only while this is
108
+ * `false`. It used to be dropped on the floor here, which made a truncated
109
+ * read indistinguishable from a complete one.
110
+ */
111
+ readonly hasMore: boolean;
112
+ /**
113
+ * The cursor to pass back as `cursor` for the next page, or `null` at the end
114
+ * of the collection.
115
+ */
116
+ readonly nextCursor: string | null;
104
117
  readonly evidence?: readonly ModelListEvidence[];
105
118
  }
106
119
 
120
+ /**
121
+ * What a collection read hands back: the rows, and where the collection stands.
122
+ *
123
+ * It is an array, so it maps, filters, spreads, and iterates like the rows it
124
+ * always was. `hasMore` and `nextCursor` ride along as non-enumerable
125
+ * properties, which keeps `JSON.stringify` and a spread producing exactly the
126
+ * array they produced before.
127
+ *
128
+ * They ride along because a list read is a page: the server applies a default
129
+ * size and caps the largest one. Returning only the rows made a truncated read
130
+ * and a complete one the same value, so the caller with 500 matching rows got
131
+ * 20 and no way to find out.
132
+ */
133
+ export type ModelList<T> = T[] & Pick<HttpTransportList<T>, 'hasMore' | 'nextCursor'>;
134
+
135
+ /**
136
+ * Attach the page state to the rows. Non-enumerable so the result stays
137
+ * indistinguishable from a plain array everywhere the properties aren't read.
138
+ */
139
+ export function modelList<T>(
140
+ rows: readonly T[],
141
+ page: Pick<HttpTransportList<unknown>, 'hasMore' | 'nextCursor'>,
142
+ ): ModelList<T> {
143
+ return Object.defineProperties([...rows], {
144
+ hasMore: { value: page.hasMore, enumerable: false },
145
+ nextCursor: { value: page.nextCursor, enumerable: false },
146
+ }) as ModelList<T>;
147
+ }
148
+
107
149
  export type IfClaimedPolicy = 'return' | 'fail';
108
150
 
109
151
  export interface ClaimedOptions {
@@ -170,6 +212,7 @@ export interface CommitOperationInput {
170
212
  readonly model: string;
171
213
  readonly id?: string | null;
172
214
  readonly data?: Record<string, unknown> | null;
215
+ readonly where?: Record<string, unknown> | null;
173
216
  readonly transactionId?: string | null;
174
217
  /** Claim identity derived from a held claim; not an application id. */
175
218
  readonly claimId?: string | null;
@@ -432,8 +475,8 @@ export interface CreateUserSessionParams<S extends SchemaRecord> {
432
475
 
433
476
  /** Mint params for a scoped **agent** session — mints a restricted `rk_` token
434
477
  * gated to exactly the operations named in `can`. `can` is typed off your
435
- * schema (no magic `'task.update'` strings): `{ Task: ['update'], Report: ['read'] }`
436
- * — the SDK serializes each entry to the wire allowlist (`task.update`). */
478
+ * schema (no magic `'item.update'` strings): `{ Item: ['update'], Report: ['read'] }`
479
+ * — the SDK serializes each entry to the wire allowlist (`item.update`). */
437
480
  export interface CreateAgentSessionParams<S extends SchemaRecord> {
438
481
  /** Your agent. `id` becomes the token's `participantId`. */
439
482
  agent: { id: string };
@@ -205,7 +205,19 @@ export interface ServerReadOptions<T> {
205
205
  */
206
206
  where?: LoadWhere<T>;
207
207
  orderBy?: { [K in keyof T]?: 'asc' | 'desc' };
208
+ /**
209
+ * Rows per page. The server has its own ceiling and applies a default when
210
+ * this is omitted, so a collection larger than one page always comes back as
211
+ * a page: read `hasMore` on the result before treating it as the whole set.
212
+ */
208
213
  limit?: number;
214
+ /**
215
+ * Where to resume: the `nextCursor` from the previous page. Pass it back with
216
+ * the same `where` and `orderBy` to walk a collection; the cursor encodes the
217
+ * sort position it was issued for, so it is an opaque token rather than a row
218
+ * id, and a read that changes either starts a new walk.
219
+ */
220
+ cursor?: string;
209
221
  /**
210
222
  * `complete` waits for the server. `unknown` returns whatever is local
211
223
  * immediately and refreshes in the background.
@@ -256,7 +268,7 @@ export interface ClaimTargetOptions<T = Record<string, unknown>> {
256
268
 
257
269
  /**
258
270
  * Narrow the claim to Zod-declared fields of the row. Return one field
259
- * directly or an array for several: `fields: (task) => task.status`.
271
+ * directly or an array for several: `fields: (item) => item.status`.
260
272
  *
261
273
  * Exclusion follows the target: claims on the same row conflict only where
262
274
  * their sets intersect, so a holder on `['title']` and a holder on
@@ -313,7 +325,7 @@ export interface ClaimTargetOptions<T = Record<string, unknown>> {
313
325
  * @deprecated Prefer `contention: { signal }`.
314
326
  *
315
327
  * Abort a pending wait from outside — the same signal that cancels
316
- * everything else in the program, so a cancelled agent task or an unmounted
328
+ * everything else in the program, so a cancelled agent item or an unmounted
317
329
  * component takes its queued claim with it. Rejects with
318
330
  * {@link AbloClaimedError} (`claim_wait_aborted`); over HTTP the abort also
319
331
  * leaves the line. Ignored once the grant has arrived — a held lease is
@@ -327,7 +339,7 @@ export interface ClaimTargetOptions<T = Record<string, unknown>> {
327
339
  ttl?: Duration;
328
340
  /**
329
341
  * Keep the lease alive for the duration of real work by beating on a
330
- * cadence — the pattern for background workers whose task outlives the
342
+ * cadence — the pattern for background workers whose item outlives the
331
343
  * crash-cleanup TTL. `true` beats every third of the TTL (so two beats can
332
344
  * fail before the lease is at risk, and a crashed worker's lease still
333
345
  * lapses within one beat window); a duration such as `'2m'` sets the
@@ -431,12 +443,12 @@ export type ClaimSkipOptions<T = Record<string, unknown>> =
431
443
  * write and the SDK acquires/releases around that one mutation:
432
444
  *
433
445
  * ```ts
434
- * await ablo.tasks.update({
446
+ * await ablo.items.update({
435
447
  * id,
436
448
  * data: { title },
437
449
  * claim: {
438
- * fields: (task) => task.title,
439
- * description: 'Renaming the task to match the project brief.',
450
+ * fields: (item) => item.title,
451
+ * description: 'Renaming the item to match the project brief.',
440
452
  * },
441
453
  * });
442
454
  * ```
@@ -18,7 +18,13 @@ import { z } from 'zod';
18
18
  import { AbloValidationError } from '../errors.js';
19
19
 
20
20
  /** Primitive operand types allowed in a where clause. */
21
- export type WherePrimitive = string | number | boolean | null;
21
+ export const wherePrimitiveSchema = z.union([
22
+ z.string(),
23
+ z.number(),
24
+ z.boolean(),
25
+ z.null(),
26
+ ]);
27
+ export type WherePrimitive = z.infer<typeof wherePrimitiveSchema>;
22
28
 
23
29
  /**
24
30
  * The comparison operators a {@link WhereClause} may use: equality and
@@ -93,6 +99,22 @@ export const WHERE_LIKE_OPS: ReadonlySet<WhereOp> = new Set<WhereOp>([
93
99
  * Returns the classification the caller needs next, so the check and the branch
94
100
  * are the same statement rather than two that can disagree.
95
101
  */
102
+ /**
103
+ * Whether an operand is the array form the set operators take.
104
+ *
105
+ * `Array.isArray` answers this at runtime but not in the type system: its
106
+ * signature narrows to `any[]`, which drops both the `readonly` and the element
107
+ * type, so a compiler that reached for it lost the operand type it had just
108
+ * finished validating and handed `any` to everything downstream. Declared here
109
+ * because operand shape is this module's vocabulary — the same reason
110
+ * {@link classifyWhereOperand} lives here rather than in either plane.
111
+ */
112
+ export function isWhereOperandList(
113
+ value: WherePrimitive | readonly WherePrimitive[] | undefined,
114
+ ): value is readonly WherePrimitive[] {
115
+ return Array.isArray(value);
116
+ }
117
+
96
118
  export function classifyWhereOperand(
97
119
  op: WhereOp,
98
120
  value: unknown,
@@ -134,10 +156,27 @@ export function classifyWhereOperand(
134
156
  *
135
157
  * The value is a single primitive for scalar operators and an array of
136
158
  * primitives for IN/NOT IN.
159
+ *
160
+ * This is a schema rather than a bare type because the grammar crosses a
161
+ * boundary: a filtered read arrives at the collection route as text, and the
162
+ * route has to decide whether what it received is a clause before compiling it
163
+ * to SQL. Declaring the shape twice — once to check at the edge, once to type
164
+ * the interior — is how the two planes came to disagree about `IN`.
137
165
  */
138
- export type WhereClause =
139
- | readonly [col: string, value: WherePrimitive]
140
- | readonly [col: string, op: WhereOp, value: WherePrimitive | readonly WherePrimitive[]];
166
+ export const whereClauseSchema = z.union([
167
+ z.tuple([z.string(), wherePrimitiveSchema]).readonly(),
168
+ z
169
+ .tuple([
170
+ z.string(),
171
+ whereOpSchema,
172
+ z.union([wherePrimitiveSchema, z.array(wherePrimitiveSchema).readonly()]),
173
+ ])
174
+ .readonly(),
175
+ ]);
176
+ export type WhereClause = z.infer<typeof whereClauseSchema>;
177
+
178
+ /** A whole filter as it travels: the AND-combined list of conditions. */
179
+ export const whereClausesSchema = z.array(whereClauseSchema).readonly();
141
180
 
142
181
  /**
143
182
  * Client-facing where shape for `load({where})` and `deleteMany({where})`.
@@ -158,3 +197,30 @@ export type LoadWhere<T> =
158
197
  | Partial<T>
159
198
  | { [K in keyof T]?: T[K] | readonly T[K][] }
160
199
  | readonly WhereClause[];
200
+
201
+ /**
202
+ * Collapse either accepted {@link LoadWhere} form to the canonical clause list.
203
+ * Tuple input passes through; object input becomes one `['col', '=', val]` per
204
+ * key, or `['col', 'IN', vals]` where the value is an array.
205
+ *
206
+ * Detection: an array is tuple form, an object is object form.
207
+ *
208
+ * Every transport funnels through this. It used to live beside the WebSocket
209
+ * loader, which left the HTTP transport to walk `Object.entries` itself — and
210
+ * that copy dropped any value it found to be an object, so an `IN` filter and
211
+ * every tuple-form clause were discarded in silence and the read came back
212
+ * unfiltered.
213
+ */
214
+ export function normalizeWhere(where: unknown): readonly WhereClause[] {
215
+ if (where == null) return [];
216
+ // Tuple form — assumed to already use server-side column names.
217
+ if (Array.isArray(where)) return where as readonly WhereClause[];
218
+ if (typeof where === 'object') {
219
+ return Object.entries(where as Record<string, unknown>).map(([key, value]) =>
220
+ Array.isArray(value)
221
+ ? ([key, 'IN', value as readonly WherePrimitive[]] as WhereClause)
222
+ : ([key, value as WherePrimitive] as WhereClause),
223
+ );
224
+ }
225
+ return [];
226
+ }