@abloatai/transaction 0.37.1 → 0.38.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 (148) hide show
  1. package/dist/ai-sdk/index.d.ts +3 -1
  2. package/dist/ai-sdk/index.d.ts.map +1 -1
  3. package/dist/ai-sdk/index.js +2 -1
  4. package/dist/ai-sdk/index.js.map +1 -1
  5. package/dist/ai-sdk/modelTools.d.ts +67 -0
  6. package/dist/ai-sdk/modelTools.d.ts.map +1 -0
  7. package/dist/ai-sdk/modelTools.js +89 -0
  8. package/dist/ai-sdk/modelTools.js.map +1 -0
  9. package/dist/ai-sdk/toolOptions.d.ts +8 -0
  10. package/dist/ai-sdk/toolOptions.d.ts.map +1 -0
  11. package/dist/ai-sdk/toolOptions.js +2 -0
  12. package/dist/ai-sdk/toolOptions.js.map +1 -0
  13. package/dist/ai-sdk/{coordinatedTool.d.ts → updateTool.d.ts} +21 -28
  14. package/dist/ai-sdk/updateTool.d.ts.map +1 -0
  15. package/dist/ai-sdk/{coordinatedTool.js → updateTool.js} +39 -50
  16. package/dist/ai-sdk/updateTool.js.map +1 -0
  17. package/dist/auth/capability.d.ts +6 -0
  18. package/dist/auth/capability.d.ts.map +1 -1
  19. package/dist/auth/capability.js +4 -0
  20. package/dist/auth/capability.js.map +1 -1
  21. package/dist/auth/capabilityLifecycle.d.ts +3 -0
  22. package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
  23. package/dist/auth/identity.d.ts +6 -4
  24. package/dist/auth/identity.d.ts.map +1 -1
  25. package/dist/auth/identity.js +7 -11
  26. package/dist/auth/identity.js.map +1 -1
  27. package/dist/auth/schemas.d.ts +5 -5
  28. package/dist/auth/schemas.d.ts.map +1 -1
  29. package/dist/auth/schemas.js +5 -2
  30. package/dist/auth/schemas.js.map +1 -1
  31. package/dist/auth/sessionMint.d.ts.map +1 -1
  32. package/dist/auth/sessionMint.js +3 -0
  33. package/dist/auth/sessionMint.js.map +1 -1
  34. package/dist/branches.d.ts +251 -0
  35. package/dist/branches.d.ts.map +1 -0
  36. package/dist/branches.js +108 -0
  37. package/dist/branches.js.map +1 -0
  38. package/dist/coordination/awaitClaimGrant.d.ts +2 -0
  39. package/dist/coordination/awaitClaimGrant.d.ts.map +1 -1
  40. package/dist/coordination/awaitClaimGrant.js +15 -2
  41. package/dist/coordination/awaitClaimGrant.js.map +1 -1
  42. package/dist/coordination/schema.d.ts +2 -0
  43. package/dist/coordination/schema.d.ts.map +1 -1
  44. package/dist/coordination/schema.js +2 -0
  45. package/dist/coordination/schema.js.map +1 -1
  46. package/dist/docs/catalog.d.ts +5 -5
  47. package/dist/docs/catalog.d.ts.map +1 -1
  48. package/dist/docs/catalog.js +12 -12
  49. package/dist/docs/catalog.js.map +1 -1
  50. package/dist/footprint.d.ts +3 -5
  51. package/dist/footprint.d.ts.map +1 -1
  52. package/dist/footprint.js +0 -0
  53. package/dist/footprint.js.map +1 -1
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/keys/index.d.ts +15 -7
  59. package/dist/keys/index.d.ts.map +1 -1
  60. package/dist/keys/index.js +45 -14
  61. package/dist/keys/index.js.map +1 -1
  62. package/dist/schema/openapi.d.ts +1 -1
  63. package/dist/schema/openapi.d.ts.map +1 -1
  64. package/dist/schema/openapi.js +308 -29
  65. package/dist/schema/openapi.js.map +1 -1
  66. package/dist/server/commit.d.ts +2 -15
  67. package/dist/server/commit.d.ts.map +1 -1
  68. package/dist/source/adapter.d.ts +7 -2
  69. package/dist/source/adapter.d.ts.map +1 -1
  70. package/dist/source/adapter.js +4 -2
  71. package/dist/source/adapter.js.map +1 -1
  72. package/dist/source/adapterFactory.d.ts +10 -0
  73. package/dist/source/adapterFactory.d.ts.map +1 -0
  74. package/dist/source/adapterFactory.js +33 -0
  75. package/dist/source/adapterFactory.js.map +1 -0
  76. package/dist/source/adapterProfile.d.ts +55 -0
  77. package/dist/source/adapterProfile.d.ts.map +1 -0
  78. package/dist/source/adapterProfile.js +54 -0
  79. package/dist/source/adapterProfile.js.map +1 -0
  80. package/dist/source/adapters/drizzle.d.ts.map +1 -1
  81. package/dist/source/adapters/drizzle.js +5 -2
  82. package/dist/source/adapters/drizzle.js.map +1 -1
  83. package/dist/source/adapters/kysely.d.ts.map +1 -1
  84. package/dist/source/adapters/kysely.js +7 -4
  85. package/dist/source/adapters/kysely.js.map +1 -1
  86. package/dist/source/adapters/memory.d.ts.map +1 -1
  87. package/dist/source/adapters/memory.js +5 -2
  88. package/dist/source/adapters/memory.js.map +1 -1
  89. package/dist/source/adapters/prisma.d.ts.map +1 -1
  90. package/dist/source/adapters/prisma.js +5 -2
  91. package/dist/source/adapters/prisma.js.map +1 -1
  92. package/dist/source/conformance.d.ts.map +1 -1
  93. package/dist/source/conformance.js +18 -0
  94. package/dist/source/conformance.js.map +1 -1
  95. package/dist/source/index.d.ts +2 -0
  96. package/dist/source/index.d.ts.map +1 -1
  97. package/dist/source/index.js +2 -0
  98. package/dist/source/index.js.map +1 -1
  99. package/dist/source/types.d.ts +3 -14
  100. package/dist/source/types.d.ts.map +1 -1
  101. package/dist/source/types.js.map +1 -1
  102. package/dist/syncLog/contract.d.ts +1 -1
  103. package/dist/syncLog/contract.d.ts.map +1 -1
  104. package/dist/syncLog/contract.js +1 -2
  105. package/dist/syncLog/contract.js.map +1 -1
  106. package/dist/wire/accountResponses.d.ts +5 -9
  107. package/dist/wire/accountResponses.d.ts.map +1 -1
  108. package/dist/wire/accountResponses.js +5 -6
  109. package/dist/wire/accountResponses.js.map +1 -1
  110. package/dist/wire/delta.d.ts +11 -0
  111. package/dist/wire/delta.d.ts.map +1 -1
  112. package/dist/wire/delta.js +11 -0
  113. package/dist/wire/delta.js.map +1 -1
  114. package/dist/wire/inboundFrames.d.ts +2 -0
  115. package/dist/wire/inboundFrames.d.ts.map +1 -1
  116. package/package.json +8 -2
  117. package/src/ai-sdk/index.ts +19 -6
  118. package/src/ai-sdk/modelTools.ts +165 -0
  119. package/src/ai-sdk/toolOptions.ts +18 -0
  120. package/src/ai-sdk/{coordinatedTool.ts → updateTool.ts} +59 -69
  121. package/src/auth/capability.ts +4 -0
  122. package/src/auth/identity.ts +16 -16
  123. package/src/auth/schemas.ts +5 -2
  124. package/src/auth/sessionMint.ts +3 -0
  125. package/src/branches.ts +134 -0
  126. package/src/coordination/awaitClaimGrant.ts +18 -2
  127. package/src/coordination/schema.ts +2 -0
  128. package/src/docs/catalog.ts +24 -12
  129. package/src/footprint.ts +0 -0
  130. package/src/index.ts +1 -0
  131. package/src/keys/index.ts +56 -17
  132. package/src/schema/openapi.ts +366 -38
  133. package/src/server/commit.ts +2 -15
  134. package/src/source/adapter.ts +7 -2
  135. package/src/source/adapterFactory.ts +60 -0
  136. package/src/source/adapterProfile.ts +75 -0
  137. package/src/source/adapters/drizzle.ts +5 -2
  138. package/src/source/adapters/kysely.ts +10 -4
  139. package/src/source/adapters/memory.ts +5 -2
  140. package/src/source/adapters/prisma.ts +5 -2
  141. package/src/source/conformance.ts +18 -0
  142. package/src/source/index.ts +14 -0
  143. package/src/source/types.ts +3 -14
  144. package/src/syncLog/contract.ts +1 -2
  145. package/src/wire/accountResponses.ts +5 -6
  146. package/src/wire/delta.ts +11 -0
  147. package/dist/ai-sdk/coordinatedTool.d.ts.map +0 -1
  148. package/dist/ai-sdk/coordinatedTool.js.map +0 -1
@@ -45,6 +45,7 @@ import {
45
45
  claimReorderReplySchema,
46
46
  claimReleaseReplySchema,
47
47
  } from '../wire/claims.js';
48
+ import { errorEnvelopeSchema } from '../wire/errorEnvelope.js';
48
49
  import { modelReadResponseSchema, modelListResponseSchema } from '../wire/modelResponses.js';
49
50
  import { modelMutationRequestSchema } from '../wire/modelMutations.js';
50
51
  import { logListResponseSchema, logQuerySchema } from '../wire/feedEvent.js';
@@ -60,12 +61,21 @@ import {
60
61
  capabilityRotationResponseSchema,
61
62
  sessionRevocationResponseSchema,
62
63
  } from '../auth/capabilityLifecycle.js';
64
+ import {
65
+ branchCredentialRequestSchema,
66
+ branchCredentialResponseSchema,
67
+ branchListResponseSchema,
68
+ branchResponseSchema,
69
+ branchStatusResponseSchema,
70
+ createBranchRequestSchema,
71
+ } from '../branches.js';
72
+ import { modelClaimSchema } from '../coordination/schema.js';
63
73
 
64
74
  /** Options for {@link schemaToOpenApi} — the metadata stamped into the generated spec. */
65
75
  export interface SchemaToOpenApiOptions {
66
76
  /** Spec title. Default `"Ablo API"`. */
67
77
  readonly title?: string;
68
- /** Spec version. Default `"1.0.0"`. */
78
+ /** Spec version. Published artifacts pass the current Ablo package version. */
69
79
  readonly version?: string;
70
80
  /** API base URL. Default `"https://api.abloatai.com/api"`. */
71
81
  readonly serverUrl?: string;
@@ -73,6 +83,80 @@ export interface SchemaToOpenApiOptions {
73
83
 
74
84
  type Json = Record<string, unknown>;
75
85
 
86
+ const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete']);
87
+
88
+ /**
89
+ * Give generators a stable public name for every operation.
90
+ *
91
+ * The map is deliberately exhaustive: adding a route without naming it throws
92
+ * while rendering the document instead of letting each language generator
93
+ * invent a different method name.
94
+ */
95
+ function applyOperationIds(
96
+ paths: Json,
97
+ operationIds: Readonly<Record<string, string>>,
98
+ ): void {
99
+ const usedKeys = new Set<string>();
100
+ const usedIds = new Set<string>();
101
+
102
+ for (const [path, rawPathItem] of Object.entries(paths)) {
103
+ const pathItem = rawPathItem as Json;
104
+ for (const [method, rawOperation] of Object.entries(pathItem)) {
105
+ if (!HTTP_METHODS.has(method)) continue;
106
+ const key = `${method.toUpperCase()} ${path}`;
107
+ const operationId = operationIds[key];
108
+ if (!operationId) {
109
+ throw new Error(`OpenAPI operation ${key} has no stable operationId`);
110
+ }
111
+ if (usedIds.has(operationId)) {
112
+ throw new Error(`OpenAPI operationId ${operationId} is not unique`);
113
+ }
114
+ pathItem[method] = { ...(rawOperation as Json), operationId };
115
+ usedKeys.add(key);
116
+ usedIds.add(operationId);
117
+ }
118
+ }
119
+
120
+ const stale = Object.keys(operationIds).filter((key) => !usedKeys.has(key));
121
+ if (stale.length > 0) {
122
+ throw new Error(
123
+ `OpenAPI operationId map names operations that do not exist: ${stale.join(', ')}`,
124
+ );
125
+ }
126
+ }
127
+
128
+ const ABLO_OPERATION_IDS: Readonly<Record<string, string>> = {
129
+ 'GET /v1/models/{model}': 'listModelRows',
130
+ 'POST /v1/models/{model}': 'createModelRow',
131
+ 'GET /v1/models/{model}/{id}': 'getModelRow',
132
+ 'PATCH /v1/models/{model}/{id}': 'updateModelRow',
133
+ 'DELETE /v1/models/{model}/{id}': 'deleteModelRow',
134
+ 'POST /v1/models/{model}/{id}/claim': 'acquireModelClaim',
135
+ 'DELETE /v1/models/{model}/{id}/claim': 'releaseModelClaim',
136
+ 'POST /v1/models/{model}/{id}/claim/heartbeat': 'heartbeatModelClaim',
137
+ 'POST /v1/models/{model}/{id}/claim/reorder': 'reorderModelClaimQueue',
138
+ 'POST /v1/ephemeral_keys': 'mintEphemeralKey',
139
+ 'GET /v1/branches': 'listBranches',
140
+ 'POST /v1/branches': 'createBranch',
141
+ 'GET /v1/branches/{id}': 'getBranch',
142
+ 'DELETE /v1/branches/{id}': 'deleteBranch',
143
+ 'POST /v1/branches/{id}/credentials': 'mintBranchCredential',
144
+ 'GET /v1/branches/{id}/status': 'getBranchStatus',
145
+ 'GET /v1/claims': 'listClaims',
146
+ 'POST /v1/claims': 'acquireClaim',
147
+ 'POST /v1/claims/heartbeat': 'heartbeatClaims',
148
+ 'GET /v1/claims/{claimId}': 'getClaim',
149
+ 'DELETE /v1/claims/{claimId}': 'releaseClaim',
150
+ 'POST /v1/claims/{claimId}/heartbeat': 'heartbeatClaim',
151
+ 'POST /v1/capabilities': 'mintCapability',
152
+ 'GET /v1/capabilities/{id}': 'getCapability',
153
+ 'DELETE /v1/capabilities/{id}': 'revokeCapability',
154
+ 'POST /v1/capabilities/{id}/rotate': 'rotateCapability',
155
+ 'GET /v1/schema': 'getSchema',
156
+ 'GET /v1/logs': 'listLogEntries',
157
+ 'POST /v1/commits': 'commit',
158
+ };
159
+
76
160
  function fieldSchema(f: FieldMeta): Json {
77
161
  switch (f.type) {
78
162
  case 'number':
@@ -106,6 +190,54 @@ const jsonResp = (description: string, schema: Json): Json => ({
106
190
  description,
107
191
  content: { 'application/json': { schema } },
108
192
  });
193
+ const schemaRef = (name: string): Json => ({ $ref: `#/components/schemas/${name}` });
194
+ const namedResp = (description: string, name: string): Json =>
195
+ jsonResp(description, schemaRef(name));
196
+
197
+ /**
198
+ * Zod emits JSON Schema 2020-12. OpenAPI 3.1 accepts it, but SDK generators
199
+ * intentionally support a narrower, portable subset:
200
+ *
201
+ * - `$schema` belongs on a standalone JSON Schema document, not every embedded
202
+ * OpenAPI Schema Object;
203
+ * - `propertyNames: { type: 'string' }` is redundant for JSON objects and is
204
+ * unsupported by Stainless;
205
+ * - `additionalProperties: {}` means "any JSON value", but Stainless requires
206
+ * the equivalent, explicit `true`.
207
+ *
208
+ * Keeping this normalization at the rendering boundary lets the Zod schemas
209
+ * remain the wire authority without publishing generator-hostile syntax.
210
+ */
211
+ function portableSchema(value: unknown): unknown {
212
+ if (Array.isArray(value)) return value.map(portableSchema);
213
+ if (typeof value !== 'object' || value === null) return value;
214
+
215
+ const source = value as Record<string, unknown>;
216
+ const result: Record<string, unknown> = {};
217
+ for (const [key, child] of Object.entries(source)) {
218
+ if (key === '$schema') continue;
219
+ if (
220
+ key === 'propertyNames' &&
221
+ typeof child === 'object' &&
222
+ child !== null &&
223
+ Object.keys(child).length === 1 &&
224
+ (child as Record<string, unknown>).type === 'string'
225
+ ) {
226
+ continue;
227
+ }
228
+ if (
229
+ key === 'additionalProperties' &&
230
+ typeof child === 'object' &&
231
+ child !== null &&
232
+ Object.keys(child).length === 0
233
+ ) {
234
+ result[key] = true;
235
+ continue;
236
+ }
237
+ result[key] = portableSchema(child);
238
+ }
239
+ return result;
240
+ }
109
241
  /**
110
242
  * Derive a JSON Schema from a wire schema.
111
243
  *
@@ -116,9 +248,11 @@ const jsonResp = (description: string, schema: Json): Json => ({
116
248
  * call site rather than defaulted.
117
249
  */
118
250
  const derive = (schema: z.ZodType, io: 'input' | 'output'): Json =>
119
- z.toJSONSchema(schema, { io, unrepresentable: 'any' }) as Json;
251
+ portableSchema(
252
+ z.toJSONSchema(schema, { io, unrepresentable: 'any' }),
253
+ ) as Json;
120
254
 
121
- const commitReceipt = (): Json => jsonResp('Commit receipt', derive(commitReceiptSchema, 'output'));
255
+ const commitReceipt = (): Json => namedResp('Commit receipt', 'CommitReceipt');
122
256
 
123
257
  const modelParam = (): Json => ({
124
258
  name: 'model',
@@ -171,7 +305,16 @@ function withGenericRows(derived: Json): Json {
171
305
  */
172
306
  function queryParams(schema: z.ZodType): Json[] {
173
307
  const props = (derive(schema, 'input').properties ?? {}) as Record<string, Json>;
174
- return Object.entries(props).map(([name, s]) => ({ name, in: 'query', schema: s }));
308
+ return Object.entries(props).map(([name, s]) => ({
309
+ name,
310
+ in: 'query',
311
+ // Query strings arrive at the server as text, but OpenAPI describes the
312
+ // caller-facing value before serialization. Generated clients should take
313
+ // an integer here and encode it, not expose a stringly typed page size.
314
+ schema: name === 'limit'
315
+ ? { type: 'integer', minimum: 1 }
316
+ : s,
317
+ }));
175
318
  }
176
319
 
177
320
  /**
@@ -180,7 +323,7 @@ function queryParams(schema: z.ZodType): Json[] {
180
323
  * and a retry after a lost response deserves to know which it got.
181
324
  */
182
325
  const releaseResp = (): Json =>
183
- jsonResp('Released', derive(claimReleaseReplySchema, 'output'));
326
+ namedResp('Released', 'ClaimRelease');
184
327
 
185
328
  const claimIdParam = (): Json => ({
186
329
  name: 'claimId',
@@ -204,8 +347,12 @@ function envelope(options: SchemaToOpenApiOptions, description: string, paths: J
204
347
  openapi: '3.1.0',
205
348
  info: {
206
349
  title: options.title ?? 'Ablo API',
207
- version: options.version ?? '1.0.0',
350
+ version: options.version ?? 'development',
208
351
  description,
352
+ license: {
353
+ name: 'Apache License 2.0',
354
+ identifier: 'Apache-2.0',
355
+ },
209
356
  },
210
357
  servers: [{ url: options.serverUrl ?? `${ABLO_HOSTED_HTTP_BASE_URL}/api` }],
211
358
  security: [{ bearerAuth: [] }],
@@ -219,6 +366,90 @@ function envelope(options: SchemaToOpenApiOptions, description: string, paths: J
219
366
  };
220
367
  }
221
368
 
369
+ const ERROR_RESPONSES: Readonly<Record<string, string>> = {
370
+ '400': 'The request did not satisfy the published contract.',
371
+ '401': 'The Bearer credential is missing, malformed, or expired.',
372
+ '403': 'The credential does not authorize this operation.',
373
+ '404': 'The addressed resource does not exist in the credential scope.',
374
+ '409': 'The request conflicts with current claim, version, or idempotency state.',
375
+ '429': 'The caller exceeded an enforced rate limit.',
376
+ '500': 'The server could not complete the request.',
377
+ '503': 'A required service is temporarily unavailable.',
378
+ };
379
+
380
+ /**
381
+ * Every route passes failures through the same server error funnel. Publish
382
+ * that fact on every operation so generated transports decode all HTTP errors
383
+ * through `ErrorEnvelope`, rather than falling back to a language-specific raw
384
+ * response type.
385
+ */
386
+ function attachCanonicalErrors(paths: Json): void {
387
+ for (const rawPathItem of Object.values(paths)) {
388
+ const pathItem = rawPathItem as Json;
389
+ for (const [method, rawOperation] of Object.entries(pathItem)) {
390
+ if (!HTTP_METHODS.has(method)) continue;
391
+ const operation = rawOperation as Json;
392
+ const responses = { ...(operation.responses as Json | undefined) };
393
+ for (const [status, description] of Object.entries(ERROR_RESPONSES)) {
394
+ responses[status] = namedResp(description, 'ErrorEnvelope');
395
+ }
396
+ responses.default = namedResp(
397
+ 'An HTTP error not otherwise listed; decoded through the canonical envelope.',
398
+ 'ErrorEnvelope',
399
+ );
400
+ operation.responses = responses;
401
+ }
402
+ }
403
+ }
404
+
405
+ function abloComponentSchemas(): Record<string, Json> {
406
+ const commitReceiptSchemaJson = derive(commitReceiptSchema, 'output');
407
+ commitReceiptSchemaJson.discriminator = { propertyName: 'status' };
408
+
409
+ const schemas: Record<string, Json> = {
410
+ Cursor: {
411
+ type: 'string',
412
+ description: 'An opaque pagination position. Copy it unchanged into the next request.',
413
+ },
414
+ ErrorEnvelope: derive(errorEnvelopeSchema, 'output'),
415
+ Claim: derive(modelClaimSchema, 'output'),
416
+ ClaimAcquire: {
417
+ oneOf: [schemaRef('ClaimAcquired'), schemaRef('ClaimQueued')],
418
+ discriminator: { propertyName: 'status' },
419
+ },
420
+ ClaimAcquired: derive(claimAcquiredResponseSchema, 'output'),
421
+ ClaimQueued: derive(claimQueuedResponseSchema, 'output'),
422
+ ClaimState: derive(claimStateSchema, 'output'),
423
+ ClaimList: derive(claimListResponseSchema, 'output'),
424
+ ClaimHeartbeat: derive(claimHeartbeatReplySchema, 'output'),
425
+ ClaimHeartbeatBatch: derive(claimHeartbeatBatchReplySchema, 'output'),
426
+ ClaimRelease: derive(claimReleaseReplySchema, 'output'),
427
+ ClaimReorder: derive(claimReorderReplySchema, 'output'),
428
+ CommitReceipt: commitReceiptSchemaJson,
429
+ ModelRead: withGenericRows(derive(modelReadResponseSchema, 'output')),
430
+ ModelPage: withGenericRows(derive(modelListResponseSchema, 'output')),
431
+ LogPage: derive(logListResponseSchema, 'output'),
432
+ SchemaRead: derive(schemaReadResponseSchema, 'output'),
433
+ };
434
+
435
+ // Reuse protocol concepts inside the named envelopes too. Without these
436
+ // refs, generators invent separate anonymous Claim and cursor models for
437
+ // every operation even though the server validates one canonical shape.
438
+ const properties = (name: string): Record<string, Json> =>
439
+ (schemas[name]?.properties ?? {}) as Record<string, Json>;
440
+ properties('ClaimAcquired').claim = schemaRef('Claim');
441
+ (properties('ClaimList').data as Json).items = schemaRef('Claim');
442
+ (properties('ModelRead').claims as Json).items = schemaRef('Claim');
443
+
444
+ for (const name of ['ClaimList', 'ModelPage', 'LogPage']) {
445
+ properties(name).next_cursor = {
446
+ oneOf: [schemaRef('Cursor'), { type: 'null' }],
447
+ };
448
+ }
449
+
450
+ return schemas;
451
+ }
452
+
222
453
  /**
223
454
  * The protocol reference: the route templates the server actually serves.
224
455
  *
@@ -234,9 +465,9 @@ function envelope(options: SchemaToOpenApiOptions, description: string, paths: J
234
465
  * {@link schemaToOpenApi}.
235
466
  */
236
467
  export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
237
- const rowResp = jsonResp(
468
+ const rowResp = namedResp(
238
469
  'The row, with the watermark it was read at and who holds it.',
239
- withGenericRows(derive(modelReadResponseSchema, 'output')),
470
+ 'ModelRead',
240
471
  );
241
472
  const tags = ['models'];
242
473
 
@@ -247,10 +478,10 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
247
478
  summary: 'List rows of a model',
248
479
  parameters: [modelParam(), ...queryParams(listQuerySchema)],
249
480
  responses: {
250
- '200': jsonResp(
481
+ '200': namedResp(
251
482
  'A page of rows. `next_cursor` feeds `starting_after` on the next ' +
252
483
  'call; `stamp` is the watermark the page was read at.',
253
- withGenericRows(derive(modelListResponseSchema, 'output')),
484
+ 'ModelPage',
254
485
  ),
255
486
  },
256
487
  },
@@ -292,16 +523,16 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
292
523
  parameters: [modelParam(), idParam()],
293
524
  requestBody: optionalJsonBody(derive(claimRequestSchema, 'input')),
294
525
  responses: {
295
- '201': jsonResp(
526
+ '201': namedResp(
296
527
  'The lease is yours. `claim.fenceToken` is set when the coordinator ' +
297
528
  'minted one; carry it on writes made under the lease.',
298
- derive(claimAcquiredResponseSchema, 'output'),
529
+ 'ClaimAcquire',
299
530
  ),
300
- '202': jsonResp(
531
+ '202': namedResp(
301
532
  'The row was already held and you asked to queue. You are in line at ' +
302
533
  '`position` — heartbeat to keep the slot, and poll ' +
303
534
  '`GET /v1/claims/{claimId}` for the grant.',
304
- derive(claimQueuedResponseSchema, 'output'),
535
+ 'ClaimAcquire',
305
536
  ),
306
537
  },
307
538
  },
@@ -319,9 +550,9 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
319
550
  parameters: [modelParam(), idParam()],
320
551
  requestBody: optionalJsonBody(derive(claimHeartbeatRequestSchema, 'input')),
321
552
  responses: {
322
- '200': jsonResp(
553
+ '200': namedResp(
323
554
  'Lease extended (or queued slot refreshed)',
324
- derive(claimHeartbeatReplySchema, 'output'),
555
+ 'ClaimHeartbeat',
325
556
  ),
326
557
  },
327
558
  },
@@ -336,7 +567,7 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
336
567
  parameters: [modelParam(), idParam()],
337
568
  requestBody: optionalJsonBody(derive(claimReorderRequestSchema, 'input')),
338
569
  responses: {
339
- '200': jsonResp('Reordered', derive(claimReorderReplySchema, 'output')),
570
+ '200': namedResp('Reordered', 'ClaimReorder'),
340
571
  },
341
572
  },
342
573
  },
@@ -354,6 +585,77 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
354
585
  responses: { '201': jsonResp('The minted credential', derive(EphemeralKeyResponseSchema, 'output')) },
355
586
  },
356
587
  },
588
+ '/v1/branches': {
589
+ get: {
590
+ tags: ['branches'],
591
+ summary: 'List transaction branches for the credential project',
592
+ responses: {
593
+ '200': jsonResp(
594
+ 'The root and every active child branch.',
595
+ derive(branchListResponseSchema, 'output'),
596
+ ),
597
+ },
598
+ },
599
+ post: {
600
+ tags: ['branches'],
601
+ summary: 'Create an isolated child branch',
602
+ description:
603
+ 'The returned id is immutable; retain it for automation. The slug is a project-scoped human handle.',
604
+ requestBody: jsonBody(derive(createBranchRequestSchema, 'input')),
605
+ responses: {
606
+ '201': jsonResp('The ready branch.', derive(branchResponseSchema, 'output')),
607
+ },
608
+ },
609
+ },
610
+ '/v1/branches/{id}': {
611
+ get: {
612
+ tags: ['branches'],
613
+ summary: 'Retrieve a branch by immutable id',
614
+ parameters: [idParam()],
615
+ responses: {
616
+ '200': jsonResp('The branch.', derive(branchResponseSchema, 'output')),
617
+ },
618
+ },
619
+ delete: {
620
+ tags: ['branches'],
621
+ summary: 'Delete a non-root branch and revoke its credentials',
622
+ parameters: [idParam()],
623
+ responses: {
624
+ '200': jsonResp('The deleted branch.', derive(branchResponseSchema, 'output')),
625
+ },
626
+ },
627
+ },
628
+ '/v1/branches/{id}/credentials': {
629
+ post: {
630
+ tags: ['branches', 'credentials'],
631
+ summary: 'Mint an expiring branch-bound test credential',
632
+ parameters: [idParam()],
633
+ requestBody: optionalJsonBody(
634
+ derive(branchCredentialRequestSchema, 'input'),
635
+ ),
636
+ responses: {
637
+ '201': jsonResp(
638
+ 'A one-time plaintext credential. Do not persist it in source control.',
639
+ derive(branchCredentialResponseSchema, 'output'),
640
+ ),
641
+ },
642
+ },
643
+ },
644
+ '/v1/branches/{id}/status': {
645
+ get: {
646
+ tags: ['branches'],
647
+ summary: 'Diagnose one branch plane',
648
+ description:
649
+ 'Returns branch lifecycle, active schema, compatibility with the parent schema, safe datasource coordinates, and readiness blockers.',
650
+ parameters: [idParam()],
651
+ responses: {
652
+ '200': jsonResp(
653
+ 'The complete branch readiness view.',
654
+ derive(branchStatusResponseSchema, 'output'),
655
+ ),
656
+ },
657
+ },
658
+ },
357
659
  '/v1/claims': {
358
660
  get: {
359
661
  tags: ['claims'],
@@ -365,9 +667,9 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
365
667
  'request names both `model` and `id` — a wait line belongs to one row.',
366
668
  parameters: queryParams(claimListQuerySchema),
367
669
  responses: {
368
- '200': jsonResp(
670
+ '200': namedResp(
369
671
  'Live claims, and the wait line behind the named row.',
370
- derive(claimListResponseSchema, 'output'),
672
+ 'ClaimList',
371
673
  ),
372
674
  },
373
675
  },
@@ -379,13 +681,13 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
379
681
  'row in `target` instead of the URL. Answers identically.',
380
682
  requestBody: jsonBody(derive(claimRequestSchema, 'input')),
381
683
  responses: {
382
- '201': jsonResp(
684
+ '201': namedResp(
383
685
  'The lease is yours.',
384
- derive(claimAcquiredResponseSchema, 'output'),
686
+ 'ClaimAcquire',
385
687
  ),
386
- '202': jsonResp(
688
+ '202': namedResp(
387
689
  'Already held, and you asked to queue. You are in line at `position`.',
388
- derive(claimQueuedResponseSchema, 'output'),
690
+ 'ClaimAcquire',
389
691
  ),
390
692
  },
391
693
  },
@@ -400,9 +702,9 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
400
702
  'credential holds on this plane.',
401
703
  requestBody: optionalJsonBody(derive(claimHeartbeatRequestSchema, 'input')),
402
704
  responses: {
403
- '200': jsonResp(
705
+ '200': namedResp(
404
706
  'One ack per lease extended.',
405
- derive(claimHeartbeatBatchReplySchema, 'output'),
707
+ 'ClaimHeartbeatBatch',
406
708
  ),
407
709
  },
408
710
  },
@@ -416,7 +718,7 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
416
718
  'was granted. `position` is advisory — a privileged reorder can move it ' +
417
719
  'up — so branch on `status`, never on position.',
418
720
  parameters: [claimIdParam()],
419
- responses: { '200': jsonResp('The claim state', derive(claimStateSchema, 'output')) },
721
+ responses: { '200': namedResp('The claim state', 'ClaimState') },
420
722
  },
421
723
  delete: {
422
724
  tags: ['claims'],
@@ -442,9 +744,9 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
442
744
  parameters: [claimIdParam()],
443
745
  requestBody: optionalJsonBody(derive(claimHeartbeatRequestSchema, 'input')),
444
746
  responses: {
445
- '200': jsonResp(
747
+ '200': namedResp(
446
748
  'Lease extended, or queued slot refreshed.',
447
- derive(claimHeartbeatReplySchema, 'output'),
749
+ 'ClaimHeartbeat',
448
750
  ),
449
751
  },
450
752
  },
@@ -518,9 +820,9 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
518
820
  'the shape once and poll the hashes after: a refetch only ever answers ' +
519
821
  'a push.',
520
822
  responses: {
521
- '200': jsonResp(
823
+ '200': namedResp(
522
824
  'The deployed schema, or `active: false` when nothing is pushed.',
523
- derive(schemaReadResponseSchema, 'output'),
825
+ 'SchemaRead',
524
826
  ),
525
827
  },
526
828
  },
@@ -537,11 +839,11 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
537
839
  'widen what it sees by asking.',
538
840
  parameters: queryParams(logQuerySchema),
539
841
  responses: {
540
- '200': jsonResp(
842
+ '200': namedResp(
541
843
  'A page of the feed, oldest first. Entries are discriminated on ' +
542
844
  '`object`, so a reader that meets an entry kind it does not know ' +
543
845
  'can skip it and keep paging.',
544
- derive(logListResponseSchema, 'output'),
846
+ 'LogPage',
545
847
  ),
546
848
  },
547
849
  },
@@ -557,13 +859,16 @@ export function abloOpenApi(options: SchemaToOpenApiOptions = {}): Json {
557
859
  },
558
860
  };
559
861
 
862
+ applyOperationIds(paths, ABLO_OPERATION_IDS);
863
+ attachCanonicalErrors(paths);
864
+
560
865
  return envelope(
561
866
  options,
562
867
  'The Ablo transaction layer: commit, read, and claim. `{model}` is any model ' +
563
868
  'from your pushed schema — the routes are the same whichever it is. ' +
564
869
  'Authenticate every request with your API key as a Bearer token.',
565
870
  paths,
566
- {},
871
+ abloComponentSchemas(),
567
872
  );
568
873
  }
569
874
 
@@ -582,6 +887,7 @@ export function schemaToOpenApi<S extends SchemaRecord>(
582
887
  const models: SchemaRecord = schema.models;
583
888
  const paths: Json = {};
584
889
  const schemas: Record<string, Json> = {};
890
+ const operationIds: Record<string, string> = {};
585
891
 
586
892
  for (const [key, def] of Object.entries(models)) {
587
893
  const ref: Json = { $ref: `#/components/schemas/${pascal(key)}` };
@@ -597,7 +903,24 @@ export function schemaToOpenApi<S extends SchemaRecord>(
597
903
  schemas[pascal(key)] = { type: 'object', properties, required };
598
904
  const createBody = jsonBody({ type: 'object', properties: createProps });
599
905
 
600
- paths[`/v1/models/${key}`] = {
906
+ const collectionPath = `/v1/models/${key}`;
907
+ const rowPath = `/v1/models/${key}/{id}`;
908
+ const claimPath = `/v1/models/${key}/{id}/claim`;
909
+ const heartbeatPath = `/v1/models/${key}/{id}/claim/heartbeat`;
910
+ const reorderPath = `/v1/models/${key}/{id}/claim/reorder`;
911
+ const modelName = pascal(key);
912
+
913
+ operationIds[`GET ${collectionPath}`] = `list${modelName}Rows`;
914
+ operationIds[`POST ${collectionPath}`] = `create${modelName}Row`;
915
+ operationIds[`GET ${rowPath}`] = `get${modelName}Row`;
916
+ operationIds[`PATCH ${rowPath}`] = `update${modelName}Row`;
917
+ operationIds[`DELETE ${rowPath}`] = `delete${modelName}Row`;
918
+ operationIds[`POST ${claimPath}`] = `acquire${modelName}Claim`;
919
+ operationIds[`DELETE ${claimPath}`] = `release${modelName}Claim`;
920
+ operationIds[`POST ${heartbeatPath}`] = `heartbeat${modelName}Claim`;
921
+ operationIds[`POST ${reorderPath}`] = `reorder${modelName}ClaimQueue`;
922
+
923
+ paths[collectionPath] = {
601
924
  get: {
602
925
  tags: [key],
603
926
  summary: `List ${key}`,
@@ -611,7 +934,7 @@ export function schemaToOpenApi<S extends SchemaRecord>(
611
934
  },
612
935
  post: { tags: [key], summary: `Create a ${key}`, requestBody: createBody, responses: { '200': commitReceipt() } },
613
936
  };
614
- paths[`/v1/models/${key}/{id}`] = {
937
+ paths[rowPath] = {
615
938
  get: {
616
939
  tags: [key],
617
940
  summary: `Retrieve a ${key}`,
@@ -626,14 +949,14 @@ export function schemaToOpenApi<S extends SchemaRecord>(
626
949
  patch: { tags: [key], summary: `Update a ${key}`, parameters: [idParam()], requestBody: createBody, responses: { '200': commitReceipt() } },
627
950
  delete: { tags: [key], summary: `Delete a ${key}`, parameters: [idParam()], responses: { '200': commitReceipt() } },
628
951
  };
629
- paths[`/v1/models/${key}/{id}/claim`] = {
952
+ paths[claimPath] = {
630
953
  post: { tags: [key], summary: `Claim a ${key} (acquire lease)`, parameters: [idParam()], responses: { '200': jsonResp('Claim acquired', { type: 'object' }) } },
631
954
  delete: { tags: [key], summary: `Release a ${key} claim`, parameters: [idParam()], responses: { '200': jsonResp('Released', { type: 'object' }) } },
632
955
  };
633
- paths[`/v1/models/${key}/{id}/claim/heartbeat`] = {
956
+ paths[heartbeatPath] = {
634
957
  post: { tags: [key], summary: `Heartbeat a held ${key} claim (extend the lease for long-running work)`, parameters: [idParam()], responses: { '200': jsonResp('Lease extended (or queued slot refreshed)', derive(claimHeartbeatReplySchema, 'output')) } },
635
958
  };
636
- paths[`/v1/models/${key}/{id}/claim/reorder`] = {
959
+ paths[reorderPath] = {
637
960
  post: { tags: [key], summary: `Reorder the ${key} wait-line (privileged)`, parameters: [idParam()], responses: { '200': jsonResp('Reordered', { type: 'object' }) } },
638
961
  };
639
962
  }
@@ -647,6 +970,11 @@ export function schemaToOpenApi<S extends SchemaRecord>(
647
970
  responses: { '200': commitReceipt() },
648
971
  },
649
972
  };
973
+ operationIds['POST /v1/commits'] = 'commit';
974
+ applyOperationIds(paths, operationIds);
975
+ attachCanonicalErrors(paths);
976
+
977
+ Object.assign(schemas, abloComponentSchemas());
650
978
 
651
979
  return envelope(
652
980
  options,
@@ -16,7 +16,6 @@
16
16
  import type { ParticipantKind, ConfirmationState } from '../log/syncDeltaRow.js';
17
17
  import type { ParticipantRef } from '../wire/delta.js';
18
18
  import type { CommitExecutionResultInput } from '../wire/commit.js';
19
- import type { Environment } from '../environment.js';
20
19
  import type { ReadDependency, TrackDependency } from '../coordination/schema.js';
21
20
 
22
21
  export interface CommitContext {
@@ -27,21 +26,15 @@ export interface CommitContext {
27
26
  */
28
27
  participantKind: ParticipantKind;
29
28
  organizationId: string;
29
+ /** Immutable branch selected by the authenticated credential. */
30
+ branchId: string;
30
31
  /**
31
32
  * Project scope used to route source-mode storage. When omitted, the commit
32
33
  * targets the organization's default project.
33
34
  */
34
35
  projectId?: string;
35
- /** Exact sandbox plane derived from the authenticated key, when present. */
36
- sandboxId?: string;
37
36
  /** Optional external account scope forwarded to storage resolvers. */
38
37
  accountScope?: string;
39
- /**
40
- * The environment this commit runs in. Source-mode adapters forward it to the
41
- * customer's handlers so that sandbox and production traffic can reach distinct
42
- * customer-owned stores.
43
- */
44
- environment?: Environment;
45
38
  /**
46
39
  * The sync groups this participant subscribes to, taken from the connection
47
40
  * upgrade or the capability token. Each is appended to every delta's `sync_groups`
@@ -50,12 +43,6 @@ export interface CommitContext {
50
43
  * commit fans out to just the organization and user groups.
51
44
  */
52
45
  syncGroups?: readonly string[];
53
- /**
54
- * When true, the commit does not add `org:<organizationId>` to a delta's sync
55
- * groups. Set this for sandbox writes, so that live organization subscribers do
56
- * not receive test-environment changes.
57
- */
58
- omitOrgSyncGroup?: boolean;
59
46
  /**
60
47
  * The participant on whose authority the actor acted. For a direct human commit
61
48
  * this equals the actor; for an agent commit it is the human at the root of the