@abloatai/ablo 0.24.0 → 0.26.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 (246) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +33 -0
  3. package/README.md +45 -36
  4. package/dist/BaseSyncedStore.d.ts +72 -195
  5. package/dist/BaseSyncedStore.js +240 -625
  6. package/dist/Database.d.ts +8 -9
  7. package/dist/Database.js +9 -14
  8. package/dist/LazyReferenceCollection.js +5 -0
  9. package/dist/Model.js +6 -8
  10. package/dist/ModelRegistry.d.ts +25 -6
  11. package/dist/ModelRegistry.js +12 -6
  12. package/dist/NetworkMonitor.js +1 -1
  13. package/dist/ObjectPool.js +9 -3
  14. package/dist/SyncClient.d.ts +15 -5
  15. package/dist/SyncClient.js +152 -66
  16. package/dist/SyncEngineContext.d.ts +1 -3
  17. package/dist/SyncEngineContext.js +1 -2
  18. package/dist/agent/Agent.d.ts +21 -8
  19. package/dist/agent/Agent.js +26 -13
  20. package/dist/ai-sdk/coordination-context.js +9 -3
  21. package/dist/ai-sdk/wrap.d.ts +1 -1
  22. package/dist/ai-sdk/wrap.js +1 -1
  23. package/dist/auth/credentialPolicy.d.ts +10 -2
  24. package/dist/auth/credentialSource.d.ts +1 -13
  25. package/dist/auth/credentialSource.js +6 -13
  26. package/dist/auth/index.js +3 -3
  27. package/dist/cli.cjs +583 -287
  28. package/dist/client/Ablo.d.ts +35 -764
  29. package/dist/client/Ablo.js +64 -688
  30. package/dist/client/ApiClient.d.ts +14 -1
  31. package/dist/client/ApiClient.js +63 -9
  32. package/dist/client/auth.d.ts +8 -17
  33. package/dist/client/auth.js +42 -6
  34. package/dist/client/consoleLogger.d.ts +36 -0
  35. package/dist/client/consoleLogger.js +45 -0
  36. package/dist/client/createInternalComponents.js +1 -1
  37. package/dist/client/createModelProxy.js +10 -6
  38. package/dist/client/credentialEndpoint.d.ts +63 -0
  39. package/dist/client/credentialEndpoint.js +87 -0
  40. package/dist/client/hostedEndpoints.d.ts +24 -0
  41. package/dist/client/hostedEndpoints.js +24 -0
  42. package/dist/client/httpClient.d.ts +1 -1
  43. package/dist/client/identity.js +2 -1
  44. package/dist/client/modelRegistration.d.ts +14 -0
  45. package/dist/client/modelRegistration.js +321 -0
  46. package/dist/client/options.d.ts +396 -0
  47. package/dist/client/options.js +10 -0
  48. package/dist/client/resourceTypes.d.ts +344 -0
  49. package/dist/client/resourceTypes.js +13 -0
  50. package/dist/client/schemaConfig.d.ts +56 -0
  51. package/dist/client/schemaConfig.js +188 -0
  52. package/dist/client/sessionMint.d.ts +1 -1
  53. package/dist/client/validateAbloOptions.js +1 -1
  54. package/dist/client/wsMutationExecutor.d.ts +33 -0
  55. package/dist/client/wsMutationExecutor.js +77 -0
  56. package/dist/context.js +0 -3
  57. package/dist/coordination/index.d.ts +8 -1
  58. package/dist/coordination/index.js +24 -1
  59. package/dist/coordination/trace.d.ts +0 -1
  60. package/dist/coordination/trace.js +0 -1
  61. package/dist/core/DatabaseManager.d.ts +0 -1
  62. package/dist/core/DatabaseManager.js +26 -24
  63. package/dist/core/QueryProcessor.js +7 -8
  64. package/dist/core/QueryView.d.ts +14 -2
  65. package/dist/core/QueryView.js +4 -5
  66. package/dist/core/StoreManager.d.ts +2 -2
  67. package/dist/core/StoreManager.js +5 -1
  68. package/dist/core/index.d.ts +1 -1
  69. package/dist/core/openIDBWithTimeout.js +15 -11
  70. package/dist/core/query-utils.js +1 -1
  71. package/dist/core/storeContract.d.ts +143 -0
  72. package/dist/core/storeContract.js +16 -0
  73. package/dist/errorCodes.d.ts +11 -2
  74. package/dist/errorCodes.js +155 -137
  75. package/dist/errors.d.ts +18 -7
  76. package/dist/errors.js +14 -1
  77. package/dist/index.js +5 -0
  78. package/dist/interfaces/index.d.ts +6 -20
  79. package/dist/keys/index.js +18 -9
  80. package/dist/mutators/UndoManager.js +6 -1
  81. package/dist/mutators/defineMutators.d.ts +1 -3
  82. package/dist/policy/types.d.ts +1 -1
  83. package/dist/query/client.d.ts +11 -0
  84. package/dist/query/client.js +81 -52
  85. package/dist/react/AbloProvider.d.ts +6 -10
  86. package/dist/react/AbloProvider.js +22 -15
  87. package/dist/react/context.d.ts +2 -118
  88. package/dist/react/useAblo.js +1 -1
  89. package/dist/react/useErrorListener.js +1 -1
  90. package/dist/react/useMutationFailureListener.js +1 -1
  91. package/dist/react/useMutators.js +7 -1
  92. package/dist/react/useReactive.js +0 -1
  93. package/dist/react/useUndoScope.js +2 -2
  94. package/dist/schema/ddlLock.d.ts +39 -0
  95. package/dist/schema/ddlLock.js +51 -0
  96. package/dist/schema/field.js +1 -1
  97. package/dist/schema/index.d.ts +4 -3
  98. package/dist/schema/index.js +15 -5
  99. package/dist/schema/model.d.ts +3 -3
  100. package/dist/schema/model.js +2 -2
  101. package/dist/schema/openapi.js +5 -1
  102. package/dist/schema/queries.d.ts +1 -1
  103. package/dist/schema/queries.js +1 -0
  104. package/dist/schema/residency.d.ts +35 -0
  105. package/dist/schema/residency.js +27 -0
  106. package/dist/schema/roles.d.ts +25 -0
  107. package/dist/schema/roles.js +37 -0
  108. package/dist/schema/schema.d.ts +3 -3
  109. package/dist/schema/schema.js +11 -7
  110. package/dist/schema/serialize.d.ts +8 -8
  111. package/dist/schema/sync-delta-row.d.ts +2 -2
  112. package/dist/schema/sync-delta-row.js +2 -2
  113. package/dist/server/storage-mode.d.ts +8 -1
  114. package/dist/server/storage-mode.js +14 -4
  115. package/dist/source/adapter.d.ts +1 -1
  116. package/dist/source/adapters/drizzle.js +4 -3
  117. package/dist/source/adapters/kysely.js +4 -3
  118. package/dist/source/adapters/memory.js +1 -1
  119. package/dist/source/adapters/prisma.js +5 -4
  120. package/dist/source/conformance.js +10 -8
  121. package/dist/source/connector.js +4 -2
  122. package/dist/source/factory.d.ts +106 -0
  123. package/dist/source/factory.js +273 -0
  124. package/dist/source/index.d.ts +11 -461
  125. package/dist/source/index.js +14 -416
  126. package/dist/source/next.d.ts +1 -1
  127. package/dist/source/next.js +1 -1
  128. package/dist/source/pushQueue.d.ts +1 -1
  129. package/dist/source/pushQueue.js +6 -3
  130. package/dist/source/signing.d.ts +59 -0
  131. package/dist/source/signing.js +142 -0
  132. package/dist/source/types.d.ts +334 -0
  133. package/dist/source/types.js +44 -0
  134. package/dist/stores/ObjectStore.d.ts +1 -1
  135. package/dist/stores/ObjectStore.js +23 -23
  136. package/dist/stores/SyncActionStore.d.ts +1 -1
  137. package/dist/stores/SyncActionStore.js +70 -35
  138. package/dist/surface.d.ts +1 -1
  139. package/dist/surface.js +1 -0
  140. package/dist/sync/AreaOfInterestManager.d.ts +2 -2
  141. package/dist/sync/AreaOfInterestManager.js +1 -1
  142. package/dist/sync/BootstrapHelper.d.ts +1 -3
  143. package/dist/sync/BootstrapHelper.js +29 -8
  144. package/dist/sync/ConnectionManager.d.ts +8 -0
  145. package/dist/sync/ConnectionManager.js +19 -4
  146. package/dist/sync/HydrationCoordinator.d.ts +12 -0
  147. package/dist/sync/HydrationCoordinator.js +25 -5
  148. package/dist/sync/NetworkProbe.d.ts +3 -2
  149. package/dist/sync/NetworkProbe.js +7 -6
  150. package/dist/sync/SyncWebSocket.d.ts +71 -120
  151. package/dist/sync/SyncWebSocket.js +324 -630
  152. package/dist/sync/awaitClaimGrant.js +27 -19
  153. package/dist/sync/bootstrapApply.d.ts +60 -0
  154. package/dist/sync/bootstrapApply.js +65 -0
  155. package/dist/sync/commitFrames.d.ts +43 -0
  156. package/dist/sync/commitFrames.js +94 -0
  157. package/dist/sync/createClaimStream.js +3 -2
  158. package/dist/sync/createSnapshot.d.ts +1 -3
  159. package/dist/sync/credentialLifecycle.d.ts +175 -0
  160. package/dist/sync/credentialLifecycle.js +329 -0
  161. package/dist/sync/deltaPipeline.d.ts +111 -0
  162. package/dist/sync/deltaPipeline.js +262 -0
  163. package/dist/sync/groupChange.d.ts +111 -0
  164. package/dist/sync/groupChange.js +246 -0
  165. package/dist/sync/heartbeat.d.ts +62 -0
  166. package/dist/sync/heartbeat.js +91 -0
  167. package/dist/sync/participants.d.ts +8 -8
  168. package/dist/sync/syncCursor.d.ts +44 -0
  169. package/dist/sync/syncCursor.js +59 -0
  170. package/dist/sync/syncPlan.d.ts +62 -0
  171. package/dist/sync/syncPlan.js +51 -0
  172. package/dist/sync/wsFrameHandlers.d.ts +117 -0
  173. package/dist/sync/wsFrameHandlers.js +378 -0
  174. package/dist/testing/fixtures/bootstrap.d.ts +9 -11
  175. package/dist/testing/fixtures/deltas.d.ts +1 -1
  176. package/dist/testing/fixtures/models.js +1 -0
  177. package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
  178. package/dist/testing/helpers/sync-engine-harness.js +2 -1
  179. package/dist/testing/index.d.ts +1 -1
  180. package/dist/testing/index.js +1 -1
  181. package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
  182. package/dist/testing/mocks/MockSyncContext.js +1 -32
  183. package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
  184. package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
  185. package/dist/transactions/TransactionQueue.d.ts +20 -56
  186. package/dist/transactions/TransactionQueue.js +101 -487
  187. package/dist/transactions/TransactionStore.d.ts +18 -0
  188. package/dist/transactions/TransactionStore.js +51 -0
  189. package/dist/transactions/coalesceRules.d.ts +34 -0
  190. package/dist/transactions/coalesceRules.js +117 -0
  191. package/dist/transactions/commitPayload.d.ts +134 -0
  192. package/dist/transactions/commitPayload.js +152 -0
  193. package/dist/transactions/deltaConfirmation.d.ts +60 -0
  194. package/dist/transactions/deltaConfirmation.js +223 -0
  195. package/dist/transactions/optimistic.d.ts +24 -0
  196. package/dist/transactions/optimistic.js +45 -0
  197. package/dist/transactions/persistedReplay.d.ts +93 -0
  198. package/dist/transactions/persistedReplay.js +105 -0
  199. package/dist/types/index.d.ts +3 -3
  200. package/dist/types/index.js +0 -5
  201. package/dist/types/modelData.d.ts +12 -0
  202. package/dist/types/modelData.js +11 -0
  203. package/dist/types/participant.d.ts +21 -0
  204. package/dist/types/participant.js +12 -0
  205. package/dist/types/streams.d.ts +8 -14
  206. package/dist/utils/mobx-setup.d.ts +1 -1
  207. package/dist/utils/mobx-setup.js +2 -2
  208. package/dist/wire/errorEnvelope.d.ts +11 -2
  209. package/dist/wire/errorEnvelope.js +14 -2
  210. package/dist/wire/frames.d.ts +90 -0
  211. package/dist/wire/frames.js +55 -1
  212. package/dist/wire/index.d.ts +4 -1
  213. package/dist/wire/index.js +20 -1
  214. package/dist/wire/protocol.d.ts +45 -0
  215. package/dist/wire/protocol.js +45 -0
  216. package/dist/wire/protocolVersion.d.ts +56 -0
  217. package/dist/wire/protocolVersion.js +63 -0
  218. package/docs/api-keys.md +4 -3
  219. package/docs/data-sources.md +12 -5
  220. package/docs/examples/existing-python-backend.md +3 -3
  221. package/docs/identity.md +4 -4
  222. package/docs/integration-guide.md +1 -1
  223. package/docs/react.md +1 -1
  224. package/docs/sessions.md +5 -7
  225. package/llms.txt +4 -2
  226. package/package.json +15 -13
  227. package/dist/client/index.d.ts +0 -36
  228. package/dist/client/index.js +0 -33
  229. package/dist/config/index.d.ts +0 -10
  230. package/dist/config/index.js +0 -12
  231. package/dist/interfaces/headless.d.ts +0 -95
  232. package/dist/interfaces/headless.js +0 -41
  233. package/dist/query/index.d.ts +0 -6
  234. package/dist/query/index.js +0 -5
  235. package/dist/realtime/index.d.ts +0 -10
  236. package/dist/realtime/index.js +0 -9
  237. package/dist/schema/plane.d.ts +0 -23
  238. package/dist/schema/plane.js +0 -19
  239. package/dist/server/next.d.ts +0 -51
  240. package/dist/server/next.js +0 -47
  241. package/dist/sync/OfflineFlush.d.ts +0 -9
  242. package/dist/sync/OfflineFlush.js +0 -22
  243. package/dist/sync/OfflineTransactionStore.d.ts +0 -37
  244. package/dist/sync/OfflineTransactionStore.js +0 -263
  245. package/dist/transactions/index.d.ts +0 -16
  246. package/dist/transactions/index.js +0 -7
package/dist/cli.cjs CHANGED
@@ -211978,9 +211978,9 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
211978
211978
  }
211979
211979
  });
211980
211980
 
211981
- // node_modules/@ts-morph/common/node_modules/balanced-match/dist/commonjs/index.js
211981
+ // node_modules/balanced-match/dist/commonjs/index.js
211982
211982
  var require_commonjs = __commonJS({
211983
- "node_modules/@ts-morph/common/node_modules/balanced-match/dist/commonjs/index.js"(exports2) {
211983
+ "node_modules/balanced-match/dist/commonjs/index.js"(exports2) {
211984
211984
  "use strict";
211985
211985
  init_cjs_shims();
211986
211986
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -212041,9 +212041,9 @@ var require_commonjs = __commonJS({
212041
212041
  }
212042
212042
  });
212043
212043
 
212044
- // node_modules/@ts-morph/common/node_modules/brace-expansion/dist/commonjs/index.js
212044
+ // node_modules/brace-expansion/dist/commonjs/index.js
212045
212045
  var require_commonjs2 = __commonJS({
212046
- "node_modules/@ts-morph/common/node_modules/brace-expansion/dist/commonjs/index.js"(exports2) {
212046
+ "node_modules/brace-expansion/dist/commonjs/index.js"(exports2) {
212047
212047
  "use strict";
212048
212048
  init_cjs_shims();
212049
212049
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -212120,17 +212120,19 @@ var require_commonjs2 = __commonJS({
212120
212120
  }
212121
212121
  function expand_(str, max, isTop) {
212122
212122
  const expansions = [];
212123
- const m2 = (0, balanced_match_1.balanced)("{", "}", str);
212124
- if (!m2)
212125
- return [str];
212126
- const pre = m2.pre;
212127
- const post = m2.post.length ? expand_(m2.post, max, false) : [""];
212128
- if (/\$$/.test(m2.pre)) {
212129
- for (let k3 = 0; k3 < post.length && k3 < max; k3++) {
212130
- const expansion = pre + "{" + m2.body + "}" + post[k3];
212131
- expansions.push(expansion);
212123
+ for (; ; ) {
212124
+ const m2 = (0, balanced_match_1.balanced)("{", "}", str);
212125
+ if (!m2)
212126
+ return [str];
212127
+ const pre = m2.pre;
212128
+ if (/\$$/.test(m2.pre)) {
212129
+ const post2 = m2.post.length ? expand_(m2.post, max, false) : [""];
212130
+ for (let k3 = 0; k3 < post2.length && k3 < max; k3++) {
212131
+ const expansion = pre + "{" + m2.body + "}" + post2[k3];
212132
+ expansions.push(expansion);
212133
+ }
212134
+ return expansions;
212132
212135
  }
212133
- } else {
212134
212136
  const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m2.body);
212135
212137
  const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m2.body);
212136
212138
  const isSequence = isNumericSequence || isAlphaSequence;
@@ -212138,10 +212140,12 @@ var require_commonjs2 = __commonJS({
212138
212140
  if (!isSequence && !isOptions) {
212139
212141
  if (m2.post.match(/,(?!,).*\}/)) {
212140
212142
  str = m2.pre + "{" + m2.body + escClose + m2.post;
212141
- return expand_(str, max, true);
212143
+ isTop = true;
212144
+ continue;
212142
212145
  }
212143
212146
  return [str];
212144
212147
  }
212148
+ const post = m2.post.length ? expand_(m2.post, max, false) : [""];
212145
212149
  let n;
212146
212150
  if (isSequence) {
212147
212151
  n = m2.body.split(/\.\./);
@@ -212205,15 +212209,15 @@ var require_commonjs2 = __commonJS({
212205
212209
  }
212206
212210
  }
212207
212211
  }
212212
+ return expansions;
212208
212213
  }
212209
- return expansions;
212210
212214
  }
212211
212215
  }
212212
212216
  });
212213
212217
 
212214
- // node_modules/@ts-morph/common/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js
212218
+ // node_modules/minimatch/dist/commonjs/assert-valid-pattern.js
212215
212219
  var require_assert_valid_pattern = __commonJS({
212216
- "node_modules/@ts-morph/common/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js"(exports2) {
212220
+ "node_modules/minimatch/dist/commonjs/assert-valid-pattern.js"(exports2) {
212217
212221
  "use strict";
212218
212222
  init_cjs_shims();
212219
212223
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -212231,9 +212235,9 @@ var require_assert_valid_pattern = __commonJS({
212231
212235
  }
212232
212236
  });
212233
212237
 
212234
- // node_modules/@ts-morph/common/node_modules/minimatch/dist/commonjs/brace-expressions.js
212238
+ // node_modules/minimatch/dist/commonjs/brace-expressions.js
212235
212239
  var require_brace_expressions = __commonJS({
212236
- "node_modules/@ts-morph/common/node_modules/minimatch/dist/commonjs/brace-expressions.js"(exports2) {
212240
+ "node_modules/minimatch/dist/commonjs/brace-expressions.js"(exports2) {
212237
212241
  "use strict";
212238
212242
  init_cjs_shims();
212239
212243
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -212349,9 +212353,9 @@ var require_brace_expressions = __commonJS({
212349
212353
  }
212350
212354
  });
212351
212355
 
212352
- // node_modules/@ts-morph/common/node_modules/minimatch/dist/commonjs/unescape.js
212356
+ // node_modules/minimatch/dist/commonjs/unescape.js
212353
212357
  var require_unescape = __commonJS({
212354
- "node_modules/@ts-morph/common/node_modules/minimatch/dist/commonjs/unescape.js"(exports2) {
212358
+ "node_modules/minimatch/dist/commonjs/unescape.js"(exports2) {
212355
212359
  "use strict";
212356
212360
  init_cjs_shims();
212357
212361
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -212366,9 +212370,9 @@ var require_unescape = __commonJS({
212366
212370
  }
212367
212371
  });
212368
212372
 
212369
- // node_modules/@ts-morph/common/node_modules/minimatch/dist/commonjs/ast.js
212373
+ // node_modules/minimatch/dist/commonjs/ast.js
212370
212374
  var require_ast = __commonJS({
212371
- "node_modules/@ts-morph/common/node_modules/minimatch/dist/commonjs/ast.js"(exports2) {
212375
+ "node_modules/minimatch/dist/commonjs/ast.js"(exports2) {
212372
212376
  "use strict";
212373
212377
  init_cjs_shims();
212374
212378
  var _a;
@@ -213021,9 +213025,9 @@ var require_ast = __commonJS({
213021
213025
  }
213022
213026
  });
213023
213027
 
213024
- // node_modules/@ts-morph/common/node_modules/minimatch/dist/commonjs/escape.js
213028
+ // node_modules/minimatch/dist/commonjs/escape.js
213025
213029
  var require_escape = __commonJS({
213026
- "node_modules/@ts-morph/common/node_modules/minimatch/dist/commonjs/escape.js"(exports2) {
213030
+ "node_modules/minimatch/dist/commonjs/escape.js"(exports2) {
213027
213031
  "use strict";
213028
213032
  init_cjs_shims();
213029
213033
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -213038,9 +213042,9 @@ var require_escape = __commonJS({
213038
213042
  }
213039
213043
  });
213040
213044
 
213041
- // node_modules/@ts-morph/common/node_modules/minimatch/dist/commonjs/index.js
213045
+ // node_modules/minimatch/dist/commonjs/index.js
213042
213046
  var require_commonjs3 = __commonJS({
213043
- "node_modules/@ts-morph/common/node_modules/minimatch/dist/commonjs/index.js"(exports2) {
213047
+ "node_modules/minimatch/dist/commonjs/index.js"(exports2) {
213044
213048
  "use strict";
213045
213049
  init_cjs_shims();
213046
213050
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -214561,9 +214565,9 @@ var require_is_glob = __commonJS({
214561
214565
  }
214562
214566
  });
214563
214567
 
214564
- // node_modules/glob-parent/index.js
214568
+ // node_modules/fast-glob/node_modules/glob-parent/index.js
214565
214569
  var require_glob_parent = __commonJS({
214566
- "node_modules/glob-parent/index.js"(exports2, module2) {
214570
+ "node_modules/fast-glob/node_modules/glob-parent/index.js"(exports2, module2) {
214567
214571
  "use strict";
214568
214572
  init_cjs_shims();
214569
214573
  var isGlob = require_is_glob();
@@ -276766,30 +276770,30 @@ var wire = (category, httpStatus, retryable, message, recovery) => ({ category,
276766
276770
  var client = (category, message) => ({ category, surface: "client", retryable: false, message });
276767
276771
  var ERROR_CODES = {
276768
276772
  // ── auth (401) ─────────────────────────────────────────────────────
276769
- apikey_invalid: wire("auth", 401, false, "API key is unknown or malformed."),
276770
- apikey_revoked: wire("auth", 401, false, "API key has been revoked."),
276773
+ apikey_invalid: wire("auth", 401, false, "This API key isn't one Ablo recognizes \u2014 it may be mistyped, truncated, or belong to a different environment. Check the key and try again."),
276774
+ apikey_revoked: wire("auth", 401, false, "This API key has been revoked and can no longer be used. Mint a new key from the dashboard."),
276771
276775
  // THE sync-engine access credential — the Stripe-style ephemeral key
276772
276776
  // (`ek_` for users, `rk_` for agents) minted server-side from the login and
276773
276777
  // presented as a Bearer. Its expiry is routine and re-mintable: get a fresh
276774
276778
  // key from the still-valid session and retry — NEVER a sign-out. (An agent's
276775
276779
  // expired `rk_` must not log a human out either.) This is the ONLY code on
276776
276780
  // the silent re-mint path; see RecoveryClass `access_credential_expiry`.
276777
- apikey_expired: wire("auth", 401, false, "API key has expired.", "access_credential_expiry"),
276778
- apikey_missing: wire("auth", 401, false, "No API key was supplied on the request."),
276779
- api_key_required: wire("auth", 401, false, "This operation requires an API key."),
276780
- capability_id_missing: wire("auth", 401, false, "A capability id was expected but not provided."),
276781
- exchange_failed: wire("auth", 401, false, "The API-key credential exchange was rejected."),
276782
- identity_resolve_failed: wire("auth", 401, false, "Identity resolution was rejected."),
276781
+ apikey_expired: wire("auth", 401, false, "This ephemeral API key has expired. Mint a fresh key from your still-valid session and retry the request.", "access_credential_expiry"),
276782
+ apikey_missing: wire("auth", 401, false, "The request arrived without an API key. Send one as `Authorization: Bearer <key>`."),
276783
+ api_key_required: wire("auth", 401, false, "This operation requires an API key, and none was presented. Send one as `Authorization: Bearer <key>`."),
276784
+ capability_id_missing: wire("auth", 401, false, "This request must name a capability id, but none was provided."),
276785
+ exchange_failed: wire("auth", 401, false, "The API key could not be exchanged for a working credential \u2014 the exchange was rejected. Check that the key is still valid."),
276786
+ identity_resolve_failed: wire("auth", 401, false, "The server could not resolve an identity for this credential \u2014 the identity lookup was rejected. Check that the credential is still valid."),
276783
276787
  auth_no_credentials: wire("auth", 401, false, "No recognized authentication credential was presented \u2014 no API key and no bearer JWT. Send `Authorization: Bearer <token>`."),
276784
- identity_missing_organization: wire("auth", 401, false, "Authentication succeeded but resolved to no organization context."),
276788
+ identity_missing_organization: wire("auth", 401, false, "Authentication succeeded, but the credential resolves to no organization, so requests cannot be scoped. Check that the key or token carries an organization."),
276785
276789
  // The long-lived login is gone — terminal, drives sign-out + re-auth.
276786
- session_expired: wire("auth", 401, false, "The session is invalid or expired; re-authenticate.", "session_expiry"),
276790
+ session_expired: wire("auth", 401, false, "Your session has expired or is no longer valid. Sign in again to continue.", "session_expiry"),
276787
276791
  // `jwt_invalid` is the residual fallback; the codes below split out the
276788
276792
  // specific failure modes so an integrating customer can tell "I registered
276789
276793
  // the wrong JWKS" from "my token has no org claim" from "wrong audience"
276790
276794
  // rather than getting one opaque code for all of them.
276791
- jwt_invalid: wire("auth", 401, false, "The bearer JWT could not be validated (unclassified)."),
276792
- jwt_malformed: wire("auth", 401, false, "The bearer JWT is not a well-formed JWT and could not be decoded."),
276795
+ jwt_invalid: wire("auth", 401, false, "The bearer JWT failed validation for a reason the server could not classify further. Check the token's issuer, signature, audience, and expiry."),
276796
+ jwt_malformed: wire("auth", 401, false, "The bearer token is not a well-formed JWT and could not be decoded. Check that the full, unmodified token was sent."),
276793
276797
  jwt_missing_issuer: wire("auth", 401, false, "The bearer JWT has no `iss` (issuer) claim, so it cannot be routed to a trusted issuer."),
276794
276798
  jwt_issuer_untrusted: wire("auth", 401, false, "The bearer JWT's `iss` is not a registered trusted issuer. Register it via POST /v1/trusted-issuers, or check the token's issuer claim."),
276795
276799
  jwt_signature_invalid: wire("auth", 401, false, "The bearer JWT's signature could not be verified against the issuer's JWKS (wrong key, rotated key, or forged token)."),
@@ -276801,24 +276805,24 @@ var ERROR_CODES = {
276801
276805
  // When a customer DOES present an external-IdP JWT, its expiry means
276802
276806
  // re-authenticate against that IdP, so it classifies as a session expiry
276803
276807
  // (which also keeps `isSessionErrorResponse` behaviour unchanged).
276804
- jwt_expired: wire("auth", 401, false, "The bearer JWT has expired; obtain a fresh token.", "session_expiry"),
276808
+ jwt_expired: wire("auth", 401, false, "The bearer JWT has expired. Obtain a fresh token from your identity provider and retry.", "session_expiry"),
276805
276809
  jwt_org_membership_denied: wire("auth", 403, false, "The bearer JWT's subject is not an active member of the organization in its `org_id` claim (removed, suspended, or the claim does not match a membership)."),
276806
- file_upload_auth_required: wire("auth", 401, false, "File upload requires an authenticated session."),
276807
- browser_apikey_blocked: client("auth", "Raw API keys must not be used from a browser context."),
276808
- browser_database_url_blocked: client("auth", "A database connection string must not be used from a browser context \u2014 it carries DB credentials."),
276809
- datasource_registration_failed: client("auth", "Failed to register the provided databaseUrl as a datasource."),
276810
- datasource_connection_unsupported: wire("validation", 400, false, "This deployment cannot register a direct (connection string) datasource \u2014 use the signed endpoint kind."),
276810
+ file_upload_auth_required: wire("auth", 401, false, "File uploads require an authenticated session. Sign in and retry."),
276811
+ browser_apikey_blocked: client("auth", "A raw API key was used from a browser, where anyone can read it. Keep secret keys server-side and hand the browser a short-lived ephemeral key instead."),
276812
+ browser_database_url_blocked: client("auth", "A database connection string was used from a browser context. It carries database credentials, so it must stay server-side."),
276813
+ datasource_registration_failed: client("auth", "The provided `databaseUrl` could not be registered as a data source. Check that the connection string is valid and the database is reachable."),
276814
+ datasource_connection_unsupported: wire("validation", 400, false, "This deployment does not accept direct connection-string data sources. Register a signed Data Source endpoint instead."),
276811
276815
  datasource_direct_deprecated: wire("validation", 410, false, "The direct (connection string) datasource is deprecated. Register a signed Data Source endpoint instead \u2014 your app owns the write and your credentials never leave it."),
276812
276816
  // ── permission / capability (403) ──────────────────────────────────
276813
- capability_scope_denied: wire("capability", 403, false, "The connection's resolved scope does not cover the attempted action."),
276814
- issuer_register_forbidden: wire("permission", 403, false, "Registering a trusted issuer requires a secret (sk_) API key."),
276815
- capability_invalid: wire("capability", 403, false, "The capability is unknown, revoked, or expired."),
276817
+ capability_scope_denied: wire("capability", 403, false, "This action falls outside the scope granted to the connection, so it was denied."),
276818
+ issuer_register_forbidden: wire("permission", 403, false, "Registering a trusted issuer requires a secret (`sk_`) API key. The key presented is not a secret key."),
276819
+ capability_invalid: wire("capability", 403, false, "This capability cannot be used \u2014 it is unknown, revoked, or expired. Request a fresh grant."),
276816
276820
  test_database_not_registered: wire("permission", 403, false, "Test mode requires a registered dev database for this org \u2014 run `npx ablo init`, or construct the client with `databaseUrl` using your test key."),
276817
276821
  tenant_routing_failed: wire("server", 500, true, "The org's registered database could not be resolved or dialed. Ablo never falls back to shared storage for a dedicated tenant \u2014 retry, and check the datasource status if it persists."),
276818
- database_role_cannot_enforce_rls: wire("permission", 403, false, "The connected database role cannot enforce row-level security (superuser or BYPASSRLS)."),
276819
- database_role_unreadable: wire("permission", 403, false, "The connected database role could not be introspected."),
276820
- database_tables_unforced_rls: wire("permission", 403, false, "Synced tables in the connected database do not have FORCE ROW LEVEL SECURITY applied."),
276821
- database_host_not_allowed: wire("permission", 403, false, "The connected database host resolves to a private, loopback, or link-local address and cannot be used."),
276822
+ database_role_cannot_enforce_rls: wire("permission", 403, false, "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."),
276823
+ database_role_unreadable: wire("permission", 403, false, "Ablo could not introspect the database role it connects with, so it cannot verify that row-level security is enforced."),
276824
+ database_tables_unforced_rls: wire("permission", 403, false, "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."),
276825
+ database_host_not_allowed: wire("permission", 403, false, "The database host resolves to a private, loopback, or link-local address, which Ablo's servers will not connect to. Use a publicly resolvable host."),
276822
276826
  // Deprecated spellings of the `database_*` codes above — still emitted by
276823
276827
  // older servers; kept so they classify identically. Do not use in new code.
276824
276828
  byo_role_cannot_enforce_rls: wire("permission", 403, false, "The direct Postgres connector role cannot enforce row-level security."),
@@ -276835,50 +276839,60 @@ var ERROR_CODES = {
276835
276839
  // behind the holder) or re-read and rebase. `retryable: true` here turned
276836
276840
  // every cross-client claim conflict into an infinite client resend loop
276837
276841
  // (~150ms storm — found by the claims journey, 2026-06-10).
276838
- claim_conflict: wire("claim", 409, false, "The target entity is claimed by another participant."),
276839
- claim_lost: wire("claim", 409, false, "A previously held claim was lost before the write applied."),
276840
- entity_claimed: wire("claim", 409, false, "The target entity is currently claimed; write was blocked."),
276841
- malformed_claim: wire("claim", 400, false, "The claim payload was malformed."),
276842
- malformed_subscription: wire("validation", 400, false, "The update_subscription payload was malformed; expected { syncGroups: string[] }."),
276843
- model_claimed: wire("claim", 409, false, "The model instance is claimed by another participant."),
276844
- model_claimed_timeout: wire("claim", 409, false, "Timed out waiting for a model claim to clear."),
276842
+ claim_conflict: wire("claim", 409, false, "Another participant holds a claim on this row, so the write was rejected. Take a claim with `ablo.<model>.claim` to queue fairly behind the holder, or re-read and rebase."),
276843
+ claim_lost: wire("claim", 409, false, "The claim held on this row was lost before the write could apply. Re-acquire the claim and retry."),
276844
+ entity_claimed: wire("claim", 409, false, "This row is currently claimed by another participant, so the write was blocked. Queue behind the holder with `ablo.<model>.claim`, or wait for the claim to clear."),
276845
+ malformed_claim: wire("claim", 400, false, "The claim payload could not be parsed. A claim must name the model and the entity it targets; check the payload shape and resend."),
276846
+ malformed_subscription: wire("validation", 400, false, "The `update_subscription` payload was malformed; expected `{ syncGroups: string[] }`."),
276847
+ model_claimed: wire("claim", 409, false, "Another participant holds a claim on this row. Read `claim.state` to see who holds it, or queue behind them with a claim of your own."),
276848
+ model_claimed_timeout: wire("claim", 409, false, "Another participant held a claim on this row and did not release it in time. Retry, or read `claim.state` to see who holds it."),
276845
276849
  model_claim_not_configured: client("claim", "Claiming requires the collaboration runtime, which the standard Ablo({ schema, apiKey }) client wires up for every model automatically \u2014 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)."),
276846
276850
  model_watch_not_configured: client("claim", "watch() opens a presence/claim subscription and needs a live WebSocket, so it is unavailable on the HTTP transport and on model proxies built without a socket. Use the standard Ablo({ schema, apiKey }) client (default WebSocket transport)."),
276847
276851
  // ── stale context / idempotency (409) ──────────────────────────────
276848
- stale_context: wire("conflict", 409, true, "The write carried a readAt watermark that is now stale; re-read and retry."),
276852
+ stale_context: wire("conflict", 409, true, "The row changed after you read it \u2014 the write's `readAt` watermark is older than the current row version. Re-read the row and retry."),
276849
276853
  // Raised by the functional `update(id, current => next)` form once its
276850
276854
  // internal reconcile budget is exhausted — the row stayed continuously
276851
276855
  // contended. Client-side: the SDK already retried; the caller decides whether
276852
276856
  // to back off, raise `retries`, or move the row to the WebSocket transport.
276853
- contention_exhausted: client("conflict", "A functional update could not land after exhausting its reconcile budget; the row stayed continuously contended."),
276854
- update_aborted: client("conflict", "The functional update reconcile loop was aborted via its AbortSignal before the write landed."),
276855
- idempotency_conflict: wire("conflict", 409, false, "The same Idempotency-Key was reused with a different request body."),
276856
- idempotency_key_too_long: wire("validation", 400, false, "The supplied Idempotency-Key exceeds the maximum length."),
276857
+ contention_exhausted: client("conflict", "A functional update kept losing to concurrent writes and exhausted its reconcile budget. Back off and retry, raise `retries`, or move the row to the WebSocket transport."),
276858
+ update_aborted: client("conflict", "The functional update was aborted via its `AbortSignal` before the write landed; nothing was written."),
276859
+ idempotency_conflict: wire("conflict", 409, false, "This `Idempotency-Key` was already used with a different request body. Reuse a key only to retry an identical request; otherwise generate a new one."),
276860
+ idempotency_key_too_long: wire("validation", 400, false, "The supplied `Idempotency-Key` exceeds the maximum length. Use a shorter key \u2014 a UUID works well."),
276857
276861
  // ── validation (400 / 422) ─────────────────────────────────────────
276858
276862
  write_options_invalid: client("validation", "The write options (`idempotencyKey` / `label` / `wait` / `readAt` / `onStale` / `claim`) failed validation against the write-options schema."),
276859
276863
  source_operation_id_required: client("validation", "A data-source operation arrived without the entity `id` it targets."),
276860
- source_adapter_misconfigured: client("validation", "The data-source ORM adapter could not map a schema model onto the backing client (missing delegate or model)."),
276861
- source_event_invalid: client("validation", "A data-source outbox event could not be built \u2014 the operation carries no entity id and none was supplied."),
276864
+ source_adapter_misconfigured: client("validation", "The data-source ORM adapter could not map a schema model onto the backing client \u2014 the client exposes no matching delegate or model. Check that the adapter and schema agree on model names."),
276865
+ // Wire since 2026-07-01: the sync-server validates every pushed/polled
276866
+ // source event before appending to the log and rejects the whole batch
276867
+ // with this code (`param` names the offending index + field path, e.g.
276868
+ // `events[3].entityId`). Also raised client-side by
276869
+ // `sourceEventForOperation` when an outbox event cannot be built.
276870
+ source_event_invalid: wire("validation", 400, false, "A data-source event was malformed \u2014 missing or invalid id, model, entityId, type, or field value. The whole event batch was rejected and nothing was ingested; fix the offending outbox row and re-send."),
276862
276871
  duration_invalid: client("validation", 'A duration value was not a number of seconds or a "500ms" | "30s" | "3m" | "24h" string.'),
276863
276872
  schema_definition_invalid: client("validation", "A schema definition value was invalid (bad column identifier, non-finite backfill, or unsupported schema-JSON version)."),
276864
276873
  cli_invalid_arguments: client("validation", "The CLI was invoked with an unknown flag or a malformed flag value."),
276865
- turn_validation_failed: wire("validation", 422, false, "The agent turn failed server-side validation."),
276874
+ turn_validation_failed: wire("validation", 422, false, "The agent turn payload failed server-side validation and was not applied."),
276866
276875
  commit_operation_required: wire("validation", 400, false, "A commit must carry `operation` or `operations`."),
276876
+ // Wire since 2026-07-01: both commit transports (WS `commit` frame and HTTP
276877
+ // `/v1/commits`) validate every operation against `commitOperationSchema`
276878
+ // (`wire/frames.ts`) and reject the whole batch with this code. `param`
276879
+ // names the offending index + field path (e.g. `operations[3].readAt`).
276880
+ commit_operation_invalid: wire("validation", 400, false, "A commit operation failed validation against the wire commit-operation schema \u2014 wrong field type (e.g. a string `readAt`), unknown `type`, or missing `model`. The whole batch was rejected; the error names the offending operation index and field path."),
276867
276881
  commit_operation_model_required: wire("validation", 400, false, "A commit operation is missing its `model`."),
276868
- commit_operations_ambiguous: wire("validation", 400, false, "A commit supplied both `operation` and `operations`."),
276882
+ commit_operations_ambiguous: wire("validation", 400, false, "A commit supplied both `operation` and `operations`. Send one or the other, not both."),
276869
276883
  commit_too_many_operations: wire("validation", 400, false, "A commit exceeded the per-commit operation limit; split it into smaller batches."),
276870
- model_required_field_missing: wire("validation", 400, false, "A required field was absent from the model payload."),
276871
- model_identifier_missing: wire("validation", 400, false, "The model payload is missing its identifier."),
276872
- snapshot_reserved_key: wire("validation", 400, false, "A snapshot used a reserved key name."),
276873
- mesh_message_invalid_input: wire("validation", 400, false, "The mesh message failed input validation."),
276874
- mesh_message_from_id_spoof: wire("validation", 403, false, "The mesh message `from` id does not match the authenticated sender."),
276875
- mesh_message_from_kind_mismatch: wire("validation", 403, false, "The mesh message `from` kind does not match the sender."),
276876
- agent_perception_missing_context: wire("validation", 422, false, "The agent perception request lacked required context."),
276884
+ model_required_field_missing: wire("validation", 400, false, "The write is missing a field the model marks as required. Include the field and retry."),
276885
+ model_identifier_missing: wire("validation", 400, false, "The payload is missing the model's identifier, so the target row cannot be determined. Include the `id` field."),
276886
+ snapshot_reserved_key: wire("validation", 400, false, "The snapshot uses a key name that is reserved by the runtime. Rename the key and retry."),
276887
+ mesh_message_invalid_input: wire("validation", 400, false, "The mesh message payload failed input validation and was not delivered."),
276888
+ mesh_message_from_id_spoof: wire("validation", 403, false, "The mesh message's `from` id does not match the authenticated sender, so it was rejected \u2014 participants may only send as themselves."),
276889
+ mesh_message_from_kind_mismatch: wire("validation", 403, false, "The mesh message's `from` kind does not match the kind of the authenticated sender, so it was rejected."),
276890
+ agent_perception_missing_context: wire("validation", 422, false, "The agent perception request is missing context it needs to run. Include the required context fields and retry."),
276877
276891
  // ── not found (404) ────────────────────────────────────────────────
276878
- entity_not_found: wire("not_found", 404, false, "The referenced entity does not exist."),
276879
- model_not_found: wire("not_found", 404, false, "The referenced model row does not exist."),
276880
- mutate_update_entity_not_found: wire("not_found", 404, false, "The entity targeted by an update does not exist."),
276881
- task_id_missing: wire("server", 502, true, "The task-create response did not include an id."),
276892
+ entity_not_found: wire("not_found", 404, false, "No row exists with the requested id. It may have been deleted, or the id may belong to a different environment."),
276893
+ model_not_found: wire("not_found", 404, false, "No row of this model exists with the requested id. It may have been deleted, or the id may belong to a different environment."),
276894
+ mutate_update_entity_not_found: wire("not_found", 404, false, "The row targeted by this update does not exist \u2014 it may have been deleted since you read it. Re-read before retrying."),
276895
+ task_id_missing: wire("server", 502, true, "The task-create response arrived without a task id, so the result cannot be used. Retry the request."),
276882
276896
  // ── data integrity / DB constraints ────────────────────────────────
276883
276897
  // Emitted when a write is rejected by a database integrity constraint
276884
276898
  // (Postgres class-23). All NON-retryable: the same payload re-sent
@@ -276886,83 +276900,83 @@ var ERROR_CODES = {
276886
276900
  // retry. The server normalizer maps SQLSTATE → these codes and tucks the
276887
276901
  // raw constraint/column/table detail into `details` rather than leaking
276888
276902
  // the driver's message text onto the wire.
276889
- not_null_violation: wire("validation", 400, false, "A required field was missing (database not-null constraint)."),
276890
- foreign_key_violation: wire("conflict", 409, false, "A referenced entity does not exist, or is still referenced (database foreign-key constraint)."),
276891
- unique_violation: wire("conflict", 409, false, "A value violates a uniqueness constraint."),
276892
- check_violation: wire("validation", 400, false, "A value violates a database check constraint."),
276893
- constraint_violation: wire("validation", 400, false, "A database integrity constraint was violated."),
276903
+ not_null_violation: wire("validation", 400, false, "The database rejected the write because a required column was left empty \u2014 a not-null constraint. The error details name the column; supply a value and retry."),
276904
+ foreign_key_violation: wire("conflict", 409, false, "The database rejected the write on a foreign-key constraint: a referenced row does not exist, or the row being deleted is still referenced by others. The error details name the constraint."),
276905
+ unique_violation: wire("conflict", 409, false, "The write duplicates a value that must be unique \u2014 another row already holds it. Choose a different value, or update the existing row."),
276906
+ check_violation: wire("validation", 400, false, "The database rejected a value that fails one of its check constraints. The error details name the constraint; adjust the value and retry."),
276907
+ constraint_violation: wire("validation", 400, false, "The database rejected the write on an integrity constraint. The error details identify the specific constraint."),
276894
276908
  column_type_mismatch: wire("validation", 400, false, "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) \u2014 but a scalar column (integer, boolean, uuid, timestamp, \u2026) 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."),
276895
276909
  // ── tenant / unknown model (400) ───────────────────────────────────
276896
276910
  server_execute_unknown_model: wire("tenant", 400, false, "Wrote to a model the server does not know. The server keeps its own copy of the schema \u2014 run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` before writing to new or changed models."),
276897
276911
  mutate_create_unknown_model: wire("tenant", 400, false, "Created a model the server does not know. Run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` first \u2014 the server keeps its own copy of the schema."),
276898
- tenant_model_columns_unknown: wire("tenant", 400, false, "The tenant model's columns could not be resolved."),
276899
- tenant_model_missing_organization_id: wire("tenant", 400, false, "The tenant model is missing the organization_id column required for isolation."),
276912
+ tenant_model_columns_unknown: wire("tenant", 400, false, "The columns for this model could not be resolved in the tenant database, so the operation cannot be mapped onto its table."),
276913
+ tenant_model_missing_organization_id: wire("tenant", 400, false, "This model's table has no `organization_id` column, which Ablo requires to isolate rows by organization. Add the column before syncing this model."),
276900
276914
  // ── schema migration / declaration (validation) ────────────────────
276901
- schema_mutable_missing_meta: wire("schema", 400, false, "A mutable schema is missing its required meta block."),
276902
- schema_scope_kind_invalid: wire("schema", 400, false, "A scope kind in the schema is invalid."),
276903
- schema_field_not_camelcase: wire("schema", 400, false, "A schema field name is not camelCase."),
276904
- schema_field_consecutive_caps: wire("schema", 400, false, "A schema field name has consecutive capital letters."),
276915
+ schema_mutable_missing_meta: wire("schema", 400, false, "The schema is declared mutable but is missing its required `meta` block."),
276916
+ schema_scope_kind_invalid: wire("schema", 400, false, "A scope declaration in the schema uses a kind the engine does not recognize."),
276917
+ schema_field_not_camelcase: wire("schema", 400, false, "A schema field name is not camelCase. Rename the field (for example `dueDate`) \u2014 Ablo derives column names from camelCase field names."),
276918
+ schema_field_consecutive_caps: wire("schema", 400, false, "A schema field name contains consecutive capital letters, which cannot be mapped to a column name unambiguously. Write acronyms in lower case (`apiKey`, not `APIKey`)."),
276905
276919
  schema_reserved_field: client("schema", "A model redeclared a reserved base field (id, createdAt, updatedAt, organizationId, createdBy) that the SDK provides automatically."),
276906
- schema_grants_shape_invalid: wire("schema", 400, false, "A grants declaration has an invalid shape."),
276907
- schema_grants_identifier_unsafe: wire("schema", 400, false, "A grants declaration referenced an unsafe identifier."),
276908
- schema_grants_relation_kind: wire("schema", 400, false, "A grants relation referenced an invalid kind."),
276909
- schema_grants_relation_missing: wire("schema", 400, false, "A grants declaration referenced a missing relation."),
276910
- schema_grants_target_not_scope_root: wire("schema", 400, false, "A grants target is not a scope root."),
276911
- drop_field: client("schema", "Migration would drop a field (destructive classification)."),
276912
- drop_model: client("schema", "Migration would drop a model (destructive classification)."),
276913
- lossy_recreate: client("schema", "Migration would require a lossy table recreate."),
276914
- made_required: client("schema", "Migration would make an existing field required."),
276915
- required_field_added: client("schema", "Migration adds a new required field."),
276916
- enum_value_removed: client("schema", "Migration removes an enum value (destructive classification)."),
276917
- risky_cast: client("schema", "Migration would perform a risky column type cast."),
276920
+ schema_grants_shape_invalid: wire("schema", 400, false, "A `grants` declaration in the schema has an invalid shape and could not be parsed."),
276921
+ schema_grants_identifier_unsafe: wire("schema", 400, false, "A `grants` declaration references an identifier that is not safe to use in SQL. Use plain column and relation names."),
276922
+ schema_grants_relation_kind: wire("schema", 400, false, "A `grants` declaration references a relation of a kind it cannot traverse."),
276923
+ schema_grants_relation_missing: wire("schema", 400, false, "A `grants` declaration references a relation the model does not define. Check the relation name against the model."),
276924
+ schema_grants_target_not_scope_root: wire("schema", 400, false, "A `grants` declaration targets a model that is not a scope root, so access cannot be derived from it."),
276925
+ drop_field: client("schema", "This migration would drop an existing field, destroying the data stored in it."),
276926
+ drop_model: client("schema", "This migration would drop an entire model and its table, destroying the rows stored in it."),
276927
+ lossy_recreate: client("schema", "This migration can only apply by recreating the table, which would not preserve its existing rows."),
276928
+ made_required: client("schema", "This migration makes an existing optional field required, which rows without a value for it would violate."),
276929
+ required_field_added: client("schema", "This migration adds a new required field that existing rows have no value for."),
276930
+ enum_value_removed: client("schema", "This migration removes an enum value that existing rows may still hold."),
276931
+ risky_cast: client("schema", "This migration changes a column to a type its current values may not convert to cleanly."),
276918
276932
  // ── claim / lease (409 / transport) ───────────────────────────────
276919
- claim_lease_unavailable: wire("claim", 503, true, "The claim-lease coordination subsystem is unavailable; retry."),
276920
- claim_not_wired: client("claim", "Claim support was used but is not wired in this runtime."),
276921
- claim_queued: wire("claim", 409, true, "The claim was queued behind an active lease holder."),
276922
- claim_wait_aborted: wire("claim", 409, true, "Waiting for the claim lease was aborted."),
276923
- claim_wait_poll_interval_required: client("claim", "A poll interval is required when waiting on an claim."),
276924
- grant_timeout: wire("claim", 504, true, "Timed out waiting for a capability grant."),
276925
- slide_intent_missing_deck_id: wire("claim", 400, false, "A slide claim was missing its deck id."),
276926
- slide_intent_unknown_sibling: wire("claim", 400, false, "A slide claim referenced an unknown sibling slide."),
276933
+ claim_lease_unavailable: wire("claim", 503, true, "The claim-lease coordination subsystem is temporarily unavailable, so the claim could not be processed. Retry shortly."),
276934
+ claim_not_wired: client("claim", "Claims were used, but this runtime has no claim support wired in. The standard `Ablo({ schema, apiKey })` client wires it up automatically."),
276935
+ claim_queued: wire("claim", 409, true, "The claim was queued behind the current lease holder and will be granted in turn. Wait, or read `claim.queue` to see your position."),
276936
+ claim_wait_aborted: wire("claim", 409, true, "The wait for this claim lease was aborted before the lease was granted."),
276937
+ claim_wait_poll_interval_required: client("claim", "Waiting on a claim requires a poll interval, and none was provided."),
276938
+ grant_timeout: wire("claim", 504, true, "The wait for a capability grant timed out before one arrived. Retry the request."),
276939
+ slide_intent_missing_deck_id: wire("claim", 400, false, "This slide claim is missing the id of the deck it belongs to."),
276940
+ slide_intent_unknown_sibling: wire("claim", 400, false, "This slide claim references a sibling slide that does not exist in the deck."),
276927
276941
  // ── bootstrap (transport) ──────────────────────────────────────────
276928
- bootstrap_fetch_timeout: wire("bootstrap", 504, true, "The bootstrap fetch timed out."),
276929
- bootstrap_offline: wire("bootstrap", 503, true, "Bootstrap could not run because the client is offline."),
276930
- bootstrap_offline_no_cache: wire("bootstrap", 503, false, "Bootstrap is offline and no cached snapshot is available."),
276931
- bootstrap_response_invalid: wire("bootstrap", 502, true, "The bootstrap response was malformed."),
276932
- bootstrap_response_schema_invalid: wire("bootstrap", 502, true, "The bootstrap response failed schema validation."),
276942
+ bootstrap_fetch_timeout: wire("bootstrap", 504, true, "The initial bootstrap fetch timed out before the server responded. Retry shortly."),
276943
+ bootstrap_offline: wire("bootstrap", 503, true, "Bootstrap could not run because the client is offline. It can proceed once the network returns."),
276944
+ bootstrap_offline_no_cache: wire("bootstrap", 503, false, "The client is offline and no cached snapshot is available to start from, so there is no data to load until the network returns."),
276945
+ bootstrap_response_invalid: wire("bootstrap", 502, true, "The bootstrap response could not be parsed. Retrying may succeed."),
276946
+ bootstrap_response_schema_invalid: wire("bootstrap", 502, true, "The bootstrap response parsed but failed schema validation, so it was not applied. Retrying may succeed."),
276933
276947
  // ── transport / connection ─────────────────────────────────────────
276934
- exchange_malformed_response: wire("transport", 502, true, "The credential exchange returned a malformed response."),
276935
- exchange_network_error: wire("transport", 503, true, "A network error occurred during credential exchange."),
276936
- source_network_error: wire("transport", 503, true, "A network error occurred talking to the source."),
276937
- identity_network_error: wire("transport", 503, true, "A network error occurred resolving identity."),
276938
- commit_no_result: wire("transport", 504, true, "The commit was sent but no result frame arrived."),
276939
- commit_failed: wire("transport", 500, true, "The commit failed to apply."),
276940
- commit_offline_grace_expired: wire("transport", 503, false, "The offline grace window expired before the commit could be sent."),
276941
- queue_too_deep: wire("transport", 503, true, "The transaction queue exceeded its depth limit."),
276942
- flush_timeout: wire("transport", 504, true, "Timed out flushing the transaction queue."),
276943
- wait_for_timeout: wire("transport", 504, true, "A wait-for condition timed out."),
276948
+ exchange_malformed_response: wire("transport", 502, true, "The credential exchange returned a response that could not be parsed. Retrying may succeed."),
276949
+ exchange_network_error: wire("transport", 503, true, "A network error interrupted the credential exchange. Check connectivity and retry."),
276950
+ source_network_error: wire("transport", 503, true, "A network error occurred while talking to the data source. Check connectivity and retry."),
276951
+ identity_network_error: wire("transport", 503, true, "A network error occurred while resolving your identity. Check connectivity and retry."),
276952
+ commit_no_result: wire("transport", 504, true, "The commit was sent, but no result frame arrived, so its outcome is unknown. It is safe to retry."),
276953
+ commit_failed: wire("transport", 500, true, "The commit reached the server but failed to apply. Retrying may succeed."),
276954
+ commit_offline_grace_expired: wire("transport", 503, false, "The offline grace window expired before this commit could be sent, so it was not applied. Re-apply the change once the connection returns."),
276955
+ queue_too_deep: wire("transport", 503, true, "The transaction queue is over its depth limit, so new writes are being rejected until it drains. Retry shortly."),
276956
+ flush_timeout: wire("transport", 504, true, "Flushing the transaction queue timed out before every pending write was sent. Retry once connectivity stabilizes."),
276957
+ wait_for_timeout: wire("transport", 504, true, "A wait-for condition timed out before it was satisfied. Retry, or extend the timeout."),
276944
276958
  instance_at_capacity: wire("transport", 503, true, "The server is at connection capacity. Retry shortly \u2014 transient and not specific to your credentials."),
276945
- fetch_unavailable: client("transport", "No fetch implementation is available in this environment."),
276946
- base_url_missing: client("transport", "No base URL was configured for the client."),
276947
- sync_not_ready: client("transport", "A sync operation was attempted before the client was ready."),
276948
- ws_not_ready: client("transport", "A frame was sent before the WebSocket was connected."),
276959
+ fetch_unavailable: client("transport", "This environment provides no `fetch` implementation, so HTTP requests cannot be made. Run on a platform with `fetch` (Node 18+, modern browsers) or supply a polyfill."),
276960
+ base_url_missing: client("transport", "The client has no base URL configured, so it cannot address the server. Set the base URL when constructing the client."),
276961
+ sync_not_ready: client("transport", "A sync operation ran before the client finished initializing. Wait for the client to be ready before syncing."),
276962
+ ws_not_ready: client("transport", "A frame was sent before the WebSocket connection was established. Wait for the connection to open before sending."),
276949
276963
  // ── quota / rate limit (429) ──────────────────────────────────────
276950
- quota_exceeded: wire("rate_limit", 429, true, "The organization exceeded its configured usage quota."),
276964
+ quota_exceeded: wire("rate_limit", 429, true, "Your organization has used up its configured usage quota. Requests will succeed again once the quota resets or the limit is raised."),
276951
276965
  connection_limit_exceeded: wire("rate_limit", 429, true, "Too many concurrent WebSocket connections for this principal or organization. Close idle connections, or retry once others drain."),
276952
276966
  // Per-CREDENTIAL request-rate limit — the fast (RPS/burst) axis, distinct from
276953
276967
  // the slow-axis `quota_exceeded` (org daily/monthly usage). Keyed per API key,
276954
276968
  // so one noisy key backs off without affecting the rest of the org. The
276955
276969
  // `Retry-After` header carries the bucket-refill delay.
276956
- rate_limit_exceeded: wire("rate_limit", 429, true, "This API key is sending requests too quickly; slow down and retry after the indicated delay."),
276970
+ rate_limit_exceeded: wire("rate_limit", 429, true, "This API key is sending requests faster than its rate limit allows. Slow down and retry after the delay in the `Retry-After` header."),
276957
276971
  // ── server (5xx) ───────────────────────────────────────────────────
276958
- internal_error: wire("server", 500, true, "An unexpected server error occurred."),
276959
- quota_lookup_failed: wire("server", 503, true, "The quota decision could not be loaded."),
276972
+ internal_error: wire("server", 500, true, "Something went wrong on Ablo's side \u2014 an unexpected server error. It is safe to retry."),
276973
+ quota_lookup_failed: wire("server", 503, true, "The server could not load this organization's quota state, so the request was rejected rather than admitted unchecked. Retry shortly."),
276960
276974
  // The per-key rate-limiter backend (Redis) was unreachable and the API is
276961
276975
  // configured to FAIL CLOSED on that path, so the request was rejected rather
276962
276976
  // than admitted unchecked. Retryable: the next attempt re-probes the backend.
276963
276977
  rate_limiter_unavailable: wire("server", 503, true, "The rate-limiter backend is unavailable and this endpoint is configured to fail closed; retry shortly."),
276964
- turn_open_failed: wire("server", 500, true, "The agent turn failed to open."),
276965
- turn_close_failed: wire("server", 500, true, "The agent turn failed to close cleanly."),
276978
+ turn_open_failed: wire("server", 500, true, "The agent turn could not be opened on the server. It is safe to retry."),
276979
+ turn_close_failed: wire("server", 500, true, "The agent turn could not be closed cleanly on the server. It is safe to retry the close."),
276966
276980
  // ── client-only invariants (never serialized) ──────────────────────
276967
276981
  invalid_options: client("client", "The Ablo client was constructed with invalid or incomplete options."),
276968
276982
  no_ablo_provider: client("client", "An Ablo hook was used outside of an Ablo provider."),
@@ -277008,52 +277022,65 @@ var ERROR_CODES = {
277008
277022
  capability_id_required: wire("validation", 400, false, "A capability id is required for this request."),
277009
277023
  organization_mismatch: wire("permission", 403, false, "The request targeted an organization the caller is not scoped to."),
277010
277024
  project_scope_denied: wire("permission", 403, false, "The request targeted a project the caller's key is not scoped to."),
277011
- project_slug_taken: wire("validation", 409, false, "A project with this slug already exists in the organization."),
277025
+ project_slug_taken: wire("validation", 409, false, "A project with this slug already exists in the organization. Choose a different slug."),
277012
277026
  forbidden: wire("permission", 403, false, "The caller lacks permission for this operation."),
277013
- source_api_key_unresolved: wire("auth", 401, false, "The source API key could not be resolved."),
277027
+ source_api_key_unresolved: wire("auth", 401, false, "The API key presented for this data source could not be resolved to a known key. Check the key and its environment."),
277014
277028
  capability_auth_disabled: wire("server", 503, false, "Capability authentication is disabled on this server."),
277015
- provisioner_unavailable: wire("server", 503, false, "No database provisioner is configured."),
277016
- invalid_model: wire("validation", 400, false, "The request named an invalid model."),
277017
- invalid_id: wire("validation", 400, false, "The request carried an invalid id."),
277029
+ provisioner_unavailable: wire("server", 503, false, "This deployment has no database provisioner configured, so tables cannot be created here."),
277030
+ invalid_model: wire("validation", 400, false, "The model name in the request is not a valid model identifier."),
277031
+ invalid_id: wire("validation", 400, false, "The id in the request is not a valid identifier."),
277018
277032
  unknown_model: wire("tenant", 400, false, "Named a model the server does not know. Run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` \u2014 the server keeps its own copy of the schema."),
277019
- model_not_tenant_scoped: wire("tenant", 400, false, "The model is not tenant-scoped and cannot be queried this way."),
277033
+ model_not_tenant_scoped: wire("tenant", 400, false, "This model is not tenant-scoped, so it cannot be queried through the tenant-scoped read path."),
277020
277034
  schema_table_invalid: wire("schema", 500, false, "The model's table identifier is invalid."),
277021
277035
  schema_scope_invalid: wire("schema", 500, false, "The model's scope predicate could not be built."),
277022
- entity_fetch_failed: wire("server", 500, true, "The entity fetch failed."),
277023
- events_required: wire("validation", 400, false, "The request must include a non-empty events array."),
277024
- ingest_failed: wire("validation", 400, false, "The source-event ingest failed."),
277025
- migration_failed: wire("server", 500, false, "The schema migration failed to apply."),
277026
- model_query_failed: wire("validation", 400, false, "The model query failed."),
277027
- queries_required: wire("validation", 400, false, "The request must include a non-empty queries array."),
277036
+ entity_fetch_failed: wire("server", 500, true, "The server failed to fetch the requested entity. It is safe to retry."),
277037
+ events_required: wire("validation", 400, false, "The request must include a non-empty `events` array."),
277038
+ ingest_failed: wire("validation", 400, false, "The source-event batch was rejected during ingest and nothing was appended. Check the events against the expected shape and re-send."),
277039
+ migration_failed: wire("server", 500, false, "The schema migration failed while applying and did not complete."),
277040
+ schema_provisioning_forbidden: wire(
277041
+ "permission",
277042
+ 403,
277043
+ false,
277044
+ "Schema registration could not create tables in the target database: the engine is not permitted to run DDL there."
277045
+ ),
277046
+ model_query_failed: wire("validation", 400, false, "The model query failed to execute. Check the query filters and operators."),
277047
+ queries_required: wire("validation", 400, false, "The request must include a non-empty `queries` array."),
277028
277048
  query_unsupported_operator: wire("validation", 400, false, "The query used an unsupported operator."),
277029
- query_unknown_relation: wire("validation", 400, false, "The query referenced an unknown relation."),
277030
- query_relation_target_unknown: wire("schema", 500, false, "A relation targets a model the schema does not define."),
277049
+ query_invalid_like_pattern: wire("validation", 400, false, "The `LIKE` pattern must not end with an escape character."),
277050
+ query_invalid_boolean: wire("validation", 400, false, "The query compared a boolean column against an invalid boolean literal."),
277051
+ protocol_version_unsupported: wire("transport", 426, false, "The client sync-protocol version is outside the range this server supports \u2014 upgrade the SDK (or the server was rolled back mid-fleet)."),
277052
+ database_unreachable: wire("validation", 400, false, "Ablo could not reach this database to check that it can stream replication. The connection string may be wrong, the host may not be reachable from Ablo's servers, or the credentials may not be accepted."),
277053
+ database_not_replication_ready: wire("validation", 400, false, "This database is not set up for logical replication yet. Every failing item \u2014 wal_level, the publication, the replication grant, a replica identity \u2014 is listed in the error details with its exact fix. `ablo connect` prints the one-time setup; `ablo connect --check` verifies it."),
277054
+ query_unknown_relation: wire("validation", 400, false, "The query references a relation the model does not define. Check the relation name against the schema."),
277055
+ query_relation_target_unknown: wire("schema", 500, false, "A relation in the query targets a model the schema does not define."),
277031
277056
  query_invalid_identifier: wire("validation", 400, false, "The query contained an invalid identifier."),
277032
277057
  org_id_required: wire("validation", 400, false, "An organization id is required for this request."),
277033
- presence_identity_required: wire("validation", 400, false, "Both userId and organizationId are required."),
277034
- upload_fields_required: wire("validation", 400, false, "A required upload field was missing."),
277035
- upload_items_required: wire("validation", 400, false, "The request must include a non-empty items array."),
277036
- presigned_url_failed: wire("server", 500, true, "Failed to generate a presigned upload URL."),
277058
+ presence_identity_required: wire("validation", 400, false, "Presence requests must carry both `userId` and `organizationId`."),
277059
+ upload_fields_required: wire("validation", 400, false, "The upload request is missing a required field."),
277060
+ upload_items_required: wire("validation", 400, false, "The request must include a non-empty `items` array."),
277061
+ presigned_url_failed: wire("server", 500, true, "The server could not generate a presigned upload URL. It is safe to retry."),
277062
+ upload_not_configured: wire("server", 503, false, "Uploads are not configured on this deployment: the upload storage bucket and CDN domain are unset."),
277037
277063
  task_id_required: wire("validation", 400, false, "A task id is required for this request."),
277038
- claim_id_required: wire("validation", 400, false, "An claim id is required for this request."),
277064
+ claim_id_required: wire("validation", 400, false, "A claim id is required for this request."),
277039
277065
  commit_operation_action_required: wire("validation", 400, false, "A commit operation is missing its `action`."),
277040
277066
  commit_operation_unsupported: wire("validation", 400, false, "A commit operation used an unsupported `action`."),
277041
277067
  usage_invalid: wire("validation", 400, false, "The usage request was invalid."),
277042
277068
  invalid_request: wire("validation", 400, false, "The request parameters were invalid."),
277043
277069
  capability_not_found: wire("not_found", 404, false, "No capability exists with the given id."),
277044
- invalid_participant_kind: wire("validation", 400, false, "The participant kind is invalid."),
277045
- invalid_sync_group: wire("validation", 400, false, 'Sync groups must be "default" or "<namespace>:<id>".'),
277046
- narrow_scope_required: wire("validation", 400, false, "A narrowed scope is required for this request."),
277047
- wide_scope_forbidden: wire("permission", 403, false, "A wide scope is not permitted for this caller."),
277048
- capability_required: wire("auth", 401, false, "This operation requires a capability."),
277070
+ invalid_participant_kind: wire("validation", 400, false, "The participant kind is not one the server recognizes."),
277071
+ invalid_sync_group: wire("validation", 400, false, "Sync groups must be `default` or `<namespace>:<id>`."),
277072
+ narrow_scope_required: wire("validation", 400, false, "This request requires a scope narrowed to specific resources; the presented scope is too broad."),
277073
+ wide_scope_forbidden: wire("permission", 403, false, "This caller may not use a wide scope. Request a scope narrowed to the resources you need."),
277074
+ capability_required: wire("auth", 401, false, "This operation requires a capability, and none was presented."),
277049
277075
  parent_turn_not_found: wire("not_found", 404, false, "The referenced parent turn does not exist."),
277050
- parent_turn_foreign_agent: wire("permission", 403, false, "The parent turn belongs to a different agent."),
277076
+ parent_turn_foreign_agent: wire("permission", 403, false, "The referenced parent turn belongs to a different agent, so it cannot be used here."),
277051
277077
  turn_not_found: wire("not_found", 404, false, "The referenced turn does not exist."),
277052
- turn_foreign_agent: wire("permission", 403, false, "The turn belongs to a different agent."),
277053
- invalid_intent: wire("validation", 400, false, "The claim request was invalid."),
277078
+ turn_foreign_agent: wire("permission", 403, false, "This turn belongs to a different agent, so the caller cannot act on it."),
277079
+ invalid_intent: wire("validation", 400, false, "The claim request was malformed and could not be processed."),
277054
277080
  schema_too_large: wire("validation", 413, false, "The submitted schema exceeds the maximum size."),
277081
+ request_too_large: wire("validation", 413, false, "The request body exceeds the maximum size."),
277055
277082
  invalid_schema: wire("validation", 400, false, "The submitted schema could not be parsed."),
277056
- incompatible_change: wire("conflict", 409, false, "The schema change is incompatible with the current schema.")
277083
+ incompatible_change: wire("conflict", 409, false, "The schema change is incompatible with the schema currently deployed and cannot be applied as-is.")
277057
277084
  };
277058
277085
  function errorCodeSpec(code) {
277059
277086
  return ERROR_CODES[code];
@@ -279596,7 +279623,7 @@ function readProjectDatabaseUrl(cwd = process.cwd()) {
279596
279623
  for (const name of [".env.local", ".env"]) {
279597
279624
  const path = (0, import_path.resolve)(cwd, name);
279598
279625
  if (!(0, import_fs2.existsSync)(path)) continue;
279599
- const match = (0, import_fs2.readFileSync)(path, "utf8").match(/^DATABASE_URL=(.+)$/m);
279626
+ const match = /^DATABASE_URL=(.+)$/m.exec((0, import_fs2.readFileSync)(path, "utf8"));
279600
279627
  if (match?.[1]) return match[1].trim().replace(/^["']|["']$/g, "");
279601
279628
  }
279602
279629
  return null;
@@ -279606,7 +279633,7 @@ function readProjectApiKey(cwd = process.cwd()) {
279606
279633
  for (const name of [".env.local", ".env"]) {
279607
279634
  const path = (0, import_path.resolve)(cwd, name);
279608
279635
  if (!(0, import_fs2.existsSync)(path)) continue;
279609
- const match = (0, import_fs2.readFileSync)(path, "utf8").match(/^ABLO_API_KEY=(.+)$/m);
279636
+ const match = /^ABLO_API_KEY=(.+)$/m.exec((0, import_fs2.readFileSync)(path, "utf8"));
279610
279637
  if (match?.[1]) return { key: match[1].trim().replace(/^["']|["']$/g, ""), source: name };
279611
279638
  }
279612
279639
  return null;
@@ -279641,6 +279668,12 @@ var import_path3 = require("path");
279641
279668
  var import_child_process = require("child_process");
279642
279669
  var import_schema2 = require("@abloatai/ablo/schema");
279643
279670
 
279671
+ // src/client/hostedEndpoints.ts
279672
+ init_cjs_shims();
279673
+ var ABLO_HOSTED_API_DOMAIN = "api.abloatai.com";
279674
+ var ABLO_HOSTED_HTTP_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
279675
+ var ABLO_DEFAULT_BASE_URL = ABLO_HOSTED_HTTP_BASE_URL;
279676
+
279644
279677
  // src/cli/config.ts
279645
279678
  init_cjs_shims();
279646
279679
  var import_os2 = require("os");
@@ -279872,12 +279905,18 @@ function guardActiveProjectKey() {
279872
279905
  const available = Object.entries(profiles).filter(([, keys]) => hasKey(keys)).map(([name]) => name);
279873
279906
  return { ok: hasKey(profiles[activeProfile]), activeProfile, available };
279874
279907
  }
279908
+ function resolveEffectiveApiKey(modeOverride, cwd) {
279909
+ const fromProject = readProjectApiKey(cwd);
279910
+ if (fromProject) return { key: fromProject.key, source: fromProject.source };
279911
+ const key = resolveApiKey(modeOverride);
279912
+ return key ? { key, source: "stored" } : { key: void 0, source: null };
279913
+ }
279875
279914
  function resolvePushPlan() {
279876
- const envKey = process.env.ABLO_API_KEY;
279877
- if (envKey) return { flow: modeFromKey(envKey) ?? getMode(), apiKey: envKey, source: "env" };
279878
- const mode2 = getMode();
279879
- const apiKey = resolveApiKey(mode2);
279880
- return { flow: mode2, apiKey, source: apiKey ? "stored" : null };
279915
+ const { key, source } = resolveEffectiveApiKey();
279916
+ if (key != null && source != null && source !== "stored") {
279917
+ return { flow: modeFromKey(key) ?? getMode(), apiKey: key, source };
279918
+ }
279919
+ return { flow: getMode(), apiKey: key, source };
279881
279920
  }
279882
279921
 
279883
279922
  // src/cli/theme.ts
@@ -279902,7 +279941,7 @@ function coerceBackfill(raw) {
279902
279941
  }
279903
279942
  var DEFAULT_SCHEMA_PATH = "ablo/schema.ts";
279904
279943
  var DEFAULT_EXPORT = "schema";
279905
- var DEFAULT_URL = "https://api.abloatai.com";
279944
+ var DEFAULT_URL = ABLO_DEFAULT_BASE_URL;
279906
279945
  function fmtSignal(s) {
279907
279946
  const sig = s;
279908
279947
  const where = sig.field ? `${sig.model}.${sig.field}` : sig.model;
@@ -280045,7 +280084,9 @@ function schemaGitState(schemaPath) {
280045
280084
  }
280046
280085
  async function fetchActiveSchema(url, apiKey) {
280047
280086
  const ctrl = new AbortController();
280048
- const t = setTimeout(() => ctrl.abort(), 3e3);
280087
+ const t = setTimeout(() => {
280088
+ ctrl.abort();
280089
+ }, 3e3);
280049
280090
  try {
280050
280091
  const res = await fetch(`${url}/api/schema`, {
280051
280092
  headers: { authorization: `Bearer ${apiKey}` },
@@ -280112,7 +280153,7 @@ async function confirmPush(args, env) {
280112
280153
  const project = getActiveProject();
280113
280154
  const expected = project?.slug ?? "production";
280114
280155
  const typed = await he({
280115
- message: `This deploys to ${import_picocolors3.default.red(import_picocolors3.default.bold("PRODUCTION"))}. Type ${import_picocolors3.default.bold(expected)} to confirm:`,
280156
+ message: `This deploys to ${import_picocolors3.default.bold("production")}. Type ${import_picocolors3.default.bold(expected)} to confirm:`,
280116
280157
  placeholder: expected
280117
280158
  });
280118
280159
  if (pD(typed) || String(typed).trim() !== expected) {
@@ -280122,7 +280163,7 @@ async function confirmPush(args, env) {
280122
280163
  return;
280123
280164
  }
280124
280165
  if (!isProd && !args.yes && tty) {
280125
- const ok = await ye({ message: `Apply to ${import_picocolors3.default.green("sandbox")}?` });
280166
+ const ok = await ye({ message: `Apply to ${import_picocolors3.default.bold("sandbox")}?` });
280126
280167
  if (pD(ok) || !ok) {
280127
280168
  xe("Aborted.");
280128
280169
  process.exit(1);
@@ -280131,11 +280172,11 @@ async function confirmPush(args, env) {
280131
280172
  }
280132
280173
  function printPushTarget(opts) {
280133
280174
  const env = modeFromKey(opts.apiKey);
280134
- const envLabel = env === "production" ? import_picocolors3.default.red(import_picocolors3.default.bold("production")) : env === "sandbox" ? import_picocolors3.default.green("sandbox") : import_picocolors3.default.yellow("unknown env");
280175
+ const envLabel = env === "production" ? import_picocolors3.default.bold("production") : env === "sandbox" ? import_picocolors3.default.bold("sandbox") : import_picocolors3.default.yellow("unknown env");
280135
280176
  const project = getActiveProject();
280136
- const projectLabel = project ? `${import_picocolors3.default.bold(project.slug)} ${import_picocolors3.default.dim(`(${project.id})`)}` : `${import_picocolors3.default.bold("default")} ${import_picocolors3.default.dim("(org-default \u2014 `ablo projects use <slug>` to scope)")}`;
280177
+ const projectLabel = project ? `${import_picocolors3.default.bold(project.slug)} ${import_picocolors3.default.dim(`(${project.id})`)}` : `${import_picocolors3.default.bold("default")} ${import_picocolors3.default.dim("(org-default)")}`;
280137
280178
  const cliMode = getMode();
280138
- const modeNote = env && env !== cliMode ? ` ${import_picocolors3.default.yellow(`(CLI mode is ${cliMode} \u2014 this key targets ${env})`)}` : "";
280179
+ const modeNote = env && env !== cliMode ? ` ${import_picocolors3.default.yellow(`(cli mode: ${cliMode})`)}` : "";
280139
280180
  console.log(`
280140
280181
  ${brand("ablo")} ${import_picocolors3.default.dim("push")} ${import_picocolors3.default.dim("\u2192")} ${envLabel}${modeNote}`);
280141
280182
  console.log(` ${import_picocolors3.default.dim("project")} ${projectLabel}`);
@@ -280144,20 +280185,20 @@ function printPushTarget(opts) {
280144
280185
  ` ${import_picocolors3.default.dim("key")} ${maskKey(opts.apiKey)} ${import_picocolors3.default.dim(`(${describeKeySource(opts.keySource)})`)}`
280145
280186
  );
280146
280187
  console.log(
280147
- ` ${import_picocolors3.default.dim("schema")} ${import_picocolors3.default.bold(opts.schemaPath)} ${import_picocolors3.default.dim(`\xB7 ${opts.modelCount} models \xB7 hash ${opts.hash}`)}
280188
+ ` ${import_picocolors3.default.dim("schema")} ${import_picocolors3.default.bold(opts.schemaPath)} ${import_picocolors3.default.dim(`${opts.modelCount} models, hash ${opts.hash}`)}
280148
280189
  `
280149
280190
  );
280150
280191
  }
280151
280192
  function describeKeySource(source) {
280152
280193
  switch (source) {
280153
280194
  case "env":
280154
- return "ABLO_API_KEY (environment)";
280195
+ return "ABLO_API_KEY";
280155
280196
  case ".env.local":
280156
280197
  return ".env.local";
280157
280198
  case ".env":
280158
280199
  return ".env";
280159
- case "login":
280160
- return "`ablo login` (stored sandbox config)";
280200
+ case "stored":
280201
+ return "ablo login";
280161
280202
  }
280162
280203
  }
280163
280204
  async function push(argv) {
@@ -280170,14 +280211,9 @@ async function push(argv) {
280170
280211
  }
280171
280212
  let keySource = "env";
280172
280213
  if (!args.apiKey) {
280173
- const fromProject = readProjectApiKey();
280174
- if (fromProject) {
280175
- args.apiKey = fromProject.key;
280176
- keySource = fromProject.source;
280177
- } else {
280178
- args.apiKey = resolveApiKey();
280179
- keySource = "login";
280180
- }
280214
+ const resolved = resolveEffectiveApiKey();
280215
+ args.apiKey = resolved.key;
280216
+ keySource = resolved.source ?? "stored";
280181
280217
  }
280182
280218
  if (!args.apiKey) {
280183
280219
  console.error(
@@ -280268,6 +280304,12 @@ async function push(argv) {
280268
280304
  ` One or more synced tables don't have FORCE ROW LEVEL SECURITY. Run ${import_picocolors3.default.bold("npx ablo migrate")} to (re)apply the tenant policies, then re-push.`
280269
280305
  )
280270
280306
  );
280307
+ } else if (code === "capability_scope_denied" || code === "capability_invalid") {
280308
+ console.error(
280309
+ import_picocolors3.default.dim(
280310
+ ` This is a ${import_picocolors3.default.bold("database privilege")} error (Postgres 42501 / row-level security), not a key scope \u2014 a different API key won't help. The role behind this org's database can't write the target. Provision a writable, RLS-scoped role with ${import_picocolors3.default.bold("npx ablo migrate")}, or check the org's database registration. See docs/plans/read-path-logical-replication-vs-hosting.md.`
280311
+ )
280312
+ );
280271
280313
  } else if (args.apiKey != null && classifyCredentialKind(args.apiKey) === "restricted") {
280272
280314
  console.error(
280273
280315
  import_picocolors3.default.dim(
@@ -280277,7 +280319,7 @@ async function push(argv) {
280277
280319
  } else {
280278
280320
  console.error(
280279
280321
  import_picocolors3.default.dim(
280280
- ` This key isn't authorized to push schema (needs ${import_picocolors3.default.bold("schema:push")}). ` + (keySource === "login" ? `It's your stored ${import_picocolors3.default.bold("ablo login")} sandbox key \u2014 a key in ${import_picocolors3.default.bold(".env.local")} or ${import_picocolors3.default.bold("ABLO_API_KEY")} takes precedence, so put a schema:push key there (sandbox ${import_picocolors3.default.bold("sk_test_")} or production ${import_picocolors3.default.bold("sk_live_")}) and re-push. ` : `Use a schema:push key \u2014 a sandbox ${import_picocolors3.default.bold("sk_test_")} or production ${import_picocolors3.default.bold("sk_live_")}. `) + `Manage keys at https://abloatai.com`
280322
+ ` This key isn't authorized to push schema (needs ${import_picocolors3.default.bold("schema:push")}). ` + (keySource === "stored" ? `It's your stored ${import_picocolors3.default.bold("ablo login")} sandbox key \u2014 a key in ${import_picocolors3.default.bold(".env.local")} or ${import_picocolors3.default.bold("ABLO_API_KEY")} takes precedence, so put a schema:push key there (sandbox ${import_picocolors3.default.bold("sk_test_")} or production ${import_picocolors3.default.bold("sk_live_")}) and re-push. ` : `Use a schema:push key \u2014 a sandbox ${import_picocolors3.default.bold("sk_test_")} or production ${import_picocolors3.default.bold("sk_live_")}. `) + `Manage keys at https://abloatai.com`
280281
280323
  )
280282
280324
  );
280283
280325
  }
@@ -280341,32 +280383,36 @@ function planFor(schema, targetSchema = "public") {
280341
280383
  return { statements: [...plan.statements, ...adapterTables], concurrent: plan.concurrent ?? [] };
280342
280384
  }
280343
280385
  var log = {
280344
- info: (msg, fields) => console.log(`[migrate] ${msg}`, fields),
280345
- warn: (msg, fields) => console.warn(import_picocolors4.default.yellow(`[migrate] ${msg}`), fields),
280346
- error: (msg, fields) => console.error(import_picocolors4.default.red(`[migrate] ${msg}`), fields)
280386
+ info: (msg, fields) => {
280387
+ console.log(`[migrate] ${msg}`, fields);
280388
+ },
280389
+ warn: (msg, fields) => {
280390
+ console.warn(import_picocolors4.default.yellow(`[migrate] ${msg}`), fields);
280391
+ },
280392
+ error: (msg, fields) => {
280393
+ console.error(import_picocolors4.default.red(`[migrate] ${msg}`), fields);
280394
+ }
280347
280395
  };
280348
- var PG_LOCK_NOT_AVAILABLE = "55P03";
280349
- var LOCK_TIMEOUT = process.env.ABLO_SCHEMA_LOCK_TIMEOUT ?? process.env.ABLO_DDL_LOCK_TIMEOUT ?? "5s";
280350
- var MAX_LOCK_ATTEMPTS = 5;
280351
280396
  async function applyStatements(dbUrl, targetSchema, statements, concurrent = []) {
280352
280397
  const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
280353
280398
  } });
280354
280399
  const total = statements.length;
280355
280400
  const startedAt = Date.now();
280401
+ const lockTimeout = (0, import_schema3.resolveDdlLockTimeout)();
280402
+ const maxLockAttempts = (0, import_schema3.resolveDdlMaxLockAttempts)();
280356
280403
  log.info("applying migration plan", { targetSchema, statements: total });
280357
280404
  try {
280358
280405
  for (let attempt = 1; ; attempt++) {
280359
280406
  try {
280360
280407
  await sql.begin(async (tx) => {
280361
- await tx.unsafe(`SET LOCAL lock_timeout = '${LOCK_TIMEOUT}'`);
280408
+ await tx.unsafe(`SET LOCAL lock_timeout = '${lockTimeout}'`);
280362
280409
  await tx`SELECT pg_advisory_xact_lock(hashtext(${`provision:${targetSchema}`}))`;
280363
- for (let index = 0; index < total; index++) {
280364
- const statement = statements[index];
280410
+ for (const [index, statement] of statements.entries()) {
280365
280411
  try {
280366
280412
  await tx.unsafe(statement);
280367
280413
  } catch (err) {
280368
280414
  const pg = err ?? {};
280369
- if (pg.code === PG_LOCK_NOT_AVAILABLE) throw err;
280415
+ if (pg.code === import_schema3.PG_LOCK_NOT_AVAILABLE) throw err;
280370
280416
  log.error("migration plan failed", {
280371
280417
  code: "migration_failed",
280372
280418
  durationMs: Date.now() - startedAt,
@@ -280384,8 +280430,8 @@ async function applyStatements(dbUrl, targetSchema, statements, concurrent = [])
280384
280430
  break;
280385
280431
  } catch (err) {
280386
280432
  const pg = err ?? {};
280387
- if (pg.code === PG_LOCK_NOT_AVAILABLE && attempt < MAX_LOCK_ATTEMPTS) {
280388
- const backoffMs = Math.min(6e4, 10 * 2 ** attempt) + Math.floor(Math.random() * 50);
280433
+ if (pg.code === import_schema3.PG_LOCK_NOT_AVAILABLE && attempt < maxLockAttempts) {
280434
+ const backoffMs = (0, import_schema3.ddlLockRetryBackoffMs)(attempt);
280389
280435
  log.warn("schema change blocked by a lock; backing off and retrying", { targetSchema, attempt, backoffMs });
280390
280436
  await new Promise((resolve6) => setTimeout(resolve6, backoffMs));
280391
280437
  continue;
@@ -280464,6 +280510,8 @@ var ABLO_PUBLICATION = "ablo_publication";
280464
280510
  var ABLO_REPLICATION_ROLE = "ablo_replicator";
280465
280511
  function parseConnectArgs(argv) {
280466
280512
  let check2 = false;
280513
+ let register = false;
280514
+ let auditInfra = false;
280467
280515
  let tables = [];
280468
280516
  let role = ABLO_REPLICATION_ROLE;
280469
280517
  for (let i = 0; i < argv.length; i++) {
@@ -280472,6 +280520,12 @@ function parseConnectArgs(argv) {
280472
280520
  case "--check":
280473
280521
  check2 = true;
280474
280522
  break;
280523
+ case "--register":
280524
+ register = true;
280525
+ break;
280526
+ case "--audit-infra":
280527
+ auditInfra = true;
280528
+ break;
280475
280529
  case "--tables": {
280476
280530
  const value = argv[++i] ?? "";
280477
280531
  tables = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
@@ -280484,7 +280538,7 @@ function parseConnectArgs(argv) {
280484
280538
  throw new AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
280485
280539
  }
280486
280540
  }
280487
- return { check: check2, tables, role };
280541
+ return { check: check2, register, auditInfra, tables, role };
280488
280542
  }
280489
280543
  function quoteIdent(id) {
280490
280544
  return `"${id.replace(/"/g, '""')}"`;
@@ -280508,12 +280562,12 @@ function connectSetupSql(input) {
280508
280562
  function printConnectRecipe(args) {
280509
280563
  const sql = connectSetupSql({ tables: args.tables, role: args.role });
280510
280564
  console.log(`
280511
- ${brand("ablo")} ${import_picocolors5.default.dim("connect")} ${import_picocolors5.default.dim("logical replication \u2014 the one way to connect a real database")}
280565
+ ${brand("ablo")} ${import_picocolors5.default.dim("connect")} ${import_picocolors5.default.dim("logical replication \u2014 the read path (your writes stay on your own backend)")}
280512
280566
  `);
280513
280567
  console.log(
280514
- ` Ablo READS your write-ahead log (WAL) and ${import_picocolors5.default.bold("never")} runs DDL, owns, or migrates your
280515
- schema. Your app keeps writing through your own backend \u2014 Ablo only tails the changes.
280516
- Run this once against your Postgres ${import_picocolors5.default.dim("(as a superuser / the DB owner)")}:
280568
+ ` Ablo consumes your write-ahead log (WAL) via logical replication and ${import_picocolors5.default.bold("never")} runs DDL,
280569
+ owns, hosts, or migrates your schema. Your app continues to own the write path \u2014 Ablo tails
280570
+ the changes and serves them as live shapes. Run this once against your Postgres ${import_picocolors5.default.dim("(as a superuser / DB owner)")}:
280517
280571
  `
280518
280572
  );
280519
280573
  console.log(` ${import_picocolors5.default.bold("1.")} Enable logical decoding ${import_picocolors5.default.dim("(then RESTART Postgres \u2014 wal_level is not reloadable)")}`);
@@ -280549,12 +280603,24 @@ function printConnectRecipe(args) {
280549
280603
  );
280550
280604
  console.log(
280551
280605
  import_picocolors5.default.dim(
280552
- ` Reminder: Ablo never writes to your database on this path. Provisioning tables with
280553
- ${import_picocolors5.default.bold("ablo migrate")} is a separate, optional escape hatch \u2014 connecting a real database is this.`
280606
+ ` Reminder: this is the READ path \u2014 Ablo tails your WAL and never writes to your database.
280607
+ Your writes keep going through your own backend. (Ablo HOSTING your rows, or dialing in via
280608
+ ${import_picocolors5.default.bold("databaseUrl")}, is the deprecated posture \u2014 see the read-path decision doc.)`
280554
280609
  )
280555
280610
  );
280556
280611
  console.log();
280557
280612
  }
280613
+ var SYNC_INFRA_RELATIONS = [
280614
+ "sync_deltas",
280615
+ "sync_id_seq",
280616
+ "sync_metadata",
280617
+ "mutation_log"
280618
+ ];
280619
+ var SYNC_INFRA_TYPES = [
280620
+ "participant_kind",
280621
+ "backfill_provenance",
280622
+ "confirmation_state"
280623
+ ];
280558
280624
  function printCheckItem(item) {
280559
280625
  if (item.ok) {
280560
280626
  console.log(` ${import_picocolors5.default.green("\u2713")} ${item.label}`);
@@ -280568,24 +280634,28 @@ function printCheckItem(item) {
280568
280634
  async function probeReadiness(sql, opts = {}) {
280569
280635
  const publication = opts.publication ?? ABLO_PUBLICATION;
280570
280636
  const items = [];
280571
- const walRows = await sql.unsafe(`SHOW wal_level`);
280637
+ const walRows = await sql.unsafe(
280638
+ `SELECT setting FROM pg_settings WHERE name = 'wal_level'`
280639
+ );
280572
280640
  const walLevel = walRows[0]?.setting ?? "unknown";
280573
280641
  items.push(
280574
280642
  walLevel === "logical" ? { ok: true, label: `wal_level is ${import_picocolors5.default.bold("logical")}` } : {
280575
280643
  ok: false,
280576
280644
  label: `wal_level is ${import_picocolors5.default.bold(walLevel)} (need ${import_picocolors5.default.bold("logical")})`,
280577
280645
  fix: `ALTER SYSTEM SET wal_level = 'logical'; then RESTART Postgres.
280578
- On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.`
280646
+ On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.
280647
+ On Neon enable Logical Replication in the project (Console \u2192 Settings \u2192 Logical Replication, or the API) \u2014 Neon forbids ALTER SYSTEM; the toggle sets wal_level=logical.`
280579
280648
  }
280580
280649
  );
280581
280650
  const pubRows = await sql.unsafe(
280582
280651
  `SELECT puballtables FROM pg_publication WHERE pubname = $1`,
280583
280652
  [publication]
280584
280653
  );
280654
+ const pubRow = pubRows[0];
280585
280655
  items.push(
280586
- pubRows.length > 0 ? {
280656
+ pubRow ? {
280587
280657
  ok: true,
280588
- label: `publication ${import_picocolors5.default.bold(publication)} exists ${import_picocolors5.default.dim(pubRows[0].puballtables ? "(all tables)" : "(table subset)")}`
280658
+ label: `publication ${import_picocolors5.default.bold(publication)} exists ${import_picocolors5.default.dim(pubRow.puballtables ? "(all tables)" : "(table subset)")}`
280589
280659
  } : {
280590
280660
  ok: false,
280591
280661
  label: `publication ${import_picocolors5.default.bold(publication)} not found`,
@@ -280636,17 +280706,35 @@ On RDS: GRANT rds_replication TO <your_role>;`
280636
280706
  }
280637
280707
  return items;
280638
280708
  }
280639
- async function runCheck() {
280709
+ async function auditTenantSyncInfra(sql) {
280710
+ const artifacts = [];
280711
+ for (const name of SYNC_INFRA_RELATIONS) {
280712
+ const rows = await sql.unsafe(
280713
+ `SELECT to_regclass($1)::text AS reg`,
280714
+ [`public.${name}`]
280715
+ );
280716
+ artifacts.push({ kind: "relation", name, present: rows[0]?.reg != null });
280717
+ }
280718
+ for (const name of SYNC_INFRA_TYPES) {
280719
+ const rows = await sql.unsafe(
280720
+ `SELECT to_regtype($1)::text AS reg`,
280721
+ [`public.${name}`]
280722
+ );
280723
+ artifacts.push({ kind: "type", name, present: rows[0]?.reg != null });
280724
+ }
280725
+ return artifacts;
280726
+ }
280727
+ function requireDatabaseUrl(verb) {
280640
280728
  const dbUrl = readProjectDatabaseUrl();
280641
280729
  if (!dbUrl) {
280642
280730
  console.error(
280643
- import_picocolors5.default.red(" No DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors5.default.dim(` Set it to the Postgres you want Ablo to read, then re-run ${import_picocolors5.default.bold("ablo connect --check")}.`)
280731
+ import_picocolors5.default.red(" No DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors5.default.dim(` Set it to the Postgres you want Ablo to read, then re-run ${import_picocolors5.default.bold(`ablo connect ${verb}`)}.`)
280644
280732
  );
280645
280733
  process.exit(1);
280646
280734
  }
280647
- console.log(`
280648
- ${brand("ablo")} ${import_picocolors5.default.dim("connect --check")} ${import_picocolors5.default.dim("logical-replication readiness")}
280649
- `);
280735
+ return dbUrl;
280736
+ }
280737
+ async function probeAndReport(dbUrl) {
280650
280738
  const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
280651
280739
  } });
280652
280740
  let items;
@@ -280660,7 +280748,14 @@ async function runCheck() {
280660
280748
  }
280661
280749
  await sql.end({ timeout: 2 });
280662
280750
  for (const item of items) printCheckItem(item);
280663
- const failures = items.filter((i) => !i.ok).length;
280751
+ return items.filter((i) => !i.ok).length;
280752
+ }
280753
+ async function runCheck() {
280754
+ const dbUrl = requireDatabaseUrl("--check");
280755
+ console.log(`
280756
+ ${brand("ablo")} ${import_picocolors5.default.dim("connect --check")} ${import_picocolors5.default.dim("logical-replication readiness")}
280757
+ `);
280758
+ const failures = await probeAndReport(dbUrl);
280664
280759
  console.log();
280665
280760
  if (failures === 0) {
280666
280761
  console.log(` ${import_picocolors5.default.green("\u2713")} Ready \u2014 Ablo can connect and tail this database's WAL.
@@ -280673,6 +280768,114 @@ async function runCheck() {
280673
280768
  );
280674
280769
  process.exit(1);
280675
280770
  }
280771
+ async function runRegister() {
280772
+ const dbUrl = requireDatabaseUrl("--register");
280773
+ const apiKey = resolveApiKey();
280774
+ if (!apiKey) {
280775
+ console.error(
280776
+ import_picocolors5.default.red(" Not logged in.") + import_picocolors5.default.dim(` Run ${import_picocolors5.default.bold("ablo login")} (or set ${import_picocolors5.default.bold("ABLO_API_KEY")}) so Ablo knows which project to register this database for.`)
280777
+ );
280778
+ process.exit(1);
280779
+ }
280780
+ console.log(`
280781
+ ${brand("ablo")} ${import_picocolors5.default.dim("connect --register")} ${import_picocolors5.default.dim("register this database for replication")}
280782
+ `);
280783
+ const failures = await probeAndReport(dbUrl);
280784
+ if (failures > 0) {
280785
+ console.log(
280786
+ `
280787
+ ${import_picocolors5.default.red(`${failures} item${failures === 1 ? "" : "s"} to fix`)} ${import_picocolors5.default.dim("\u2014 a database that isn\u2019t replication-ready can\u2019t stream. Fix the above, then re-run.")}
280788
+ `
280789
+ );
280790
+ process.exit(1);
280791
+ }
280792
+ const apiUrl2 = (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
280793
+ let res;
280794
+ try {
280795
+ res = await fetch(`${apiUrl2}/v1/datasources`, {
280796
+ method: "POST",
280797
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
280798
+ body: JSON.stringify({ connectionString: dbUrl })
280799
+ });
280800
+ } catch (err) {
280801
+ console.error(import_picocolors5.default.red(`
280802
+ Couldn't reach ${apiUrl2}: ${err instanceof Error ? err.message : String(err)}
280803
+ `));
280804
+ process.exit(1);
280805
+ }
280806
+ if (res.ok) {
280807
+ const body2 = await res.json().catch(() => ({}));
280808
+ console.log(
280809
+ `
280810
+ ${import_picocolors5.default.green("\u2713")} Registered${body2.host ? ` ${import_picocolors5.default.dim(body2.host)}` : ""}${body2.id ? ` ${import_picocolors5.default.dim(`(${body2.id})`)}` : ""} \u2014 Ablo will replicate this database on the next sync.
280811
+ `
280812
+ );
280813
+ process.exit(0);
280814
+ }
280815
+ const body = await res.json().catch(() => ({}));
280816
+ const code = body.code ?? body.error?.code;
280817
+ const message = body.message ?? body.error?.message ?? `HTTP ${res.status}`;
280818
+ console.error(import_picocolors5.default.red(`
280819
+ Registration failed: ${message}`));
280820
+ if (code === "forbidden") {
280821
+ console.error(import_picocolors5.default.dim(` Registering a database needs a ${import_picocolors5.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors5.default.bold("ablo login")} for one.`));
280822
+ } else if (code === "datasource_connection_unsupported") {
280823
+ console.error(
280824
+ import_picocolors5.default.dim(` This deployment can\u2019t accept connection strings \u2014 use a self-hosted/hosted engine, or the signed endpoint fallback.`)
280825
+ );
280826
+ } else if (code === "database_not_replication_ready") {
280827
+ for (const f of body.details?.failures ?? []) {
280828
+ console.error(` ${import_picocolors5.default.red("\u2717")} ${import_picocolors5.default.bold(f.item ?? "item")}${f.actual ? import_picocolors5.default.dim(` (${f.actual})`) : ""}`);
280829
+ if (f.fix) for (const line of f.fix.split("\n")) console.error(` ${import_picocolors5.default.red("\u2022")} ${line}`);
280830
+ }
280831
+ console.error(import_picocolors5.default.dim(`
280832
+ Apply the fixes, verify with ${import_picocolors5.default.bold("ablo connect --check")}, then re-run.`));
280833
+ } else if (code === "database_unreachable") {
280834
+ if (body.details?.reason) console.error(import_picocolors5.default.dim(` ${body.details.reason}`));
280835
+ console.error(
280836
+ import_picocolors5.default.dim(
280837
+ ` Ablo's servers must be able to reach this database \u2014 a localhost or private-network
280838
+ Postgres can't be replicated by the hosted service. Use a reachable host (or a tunnel),
280839
+ or the signed ${import_picocolors5.default.bold("dataSource()")} endpoint fallback.`
280840
+ )
280841
+ );
280842
+ }
280843
+ console.error();
280844
+ process.exit(1);
280845
+ }
280846
+ async function runAuditInfra() {
280847
+ const dbUrl = requireDatabaseUrl("--audit-infra");
280848
+ const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
280849
+ } });
280850
+ let artifacts;
280851
+ try {
280852
+ artifacts = await auditTenantSyncInfra(sql);
280853
+ } catch (err) {
280854
+ const pg = err ?? {};
280855
+ console.error(import_picocolors5.default.red(` Couldn't audit the database: ${pg.message ?? String(err)}`));
280856
+ await sql.end({ timeout: 2 });
280857
+ process.exit(1);
280858
+ }
280859
+ await sql.end({ timeout: 2 });
280860
+ console.log(`
280861
+ ${brand("ablo")} ${import_picocolors5.default.dim("connect --audit-infra")} ${import_picocolors5.default.dim("Stage 5 tenant DB sync-infra audit")}
280862
+ `);
280863
+ const present = artifacts.filter((a) => a.present);
280864
+ if (present.length === 0) {
280865
+ console.log(` ${import_picocolors5.default.green("\u2713")} No deprecated Ablo sync infrastructure found in public.
280866
+ `);
280867
+ process.exit(0);
280868
+ }
280869
+ for (const artifact of present) {
280870
+ const label = artifact.kind === "type" ? "type" : "relation";
280871
+ console.log(` ${import_picocolors5.default.yellow("!")} ${label} ${import_picocolors5.default.bold(`public.${artifact.name}`)} exists`);
280872
+ }
280873
+ console.log(
280874
+ `
280875
+ ${import_picocolors5.default.yellow(`${present.length} artifact${present.length === 1 ? "" : "s"} found`)} ` + import_picocolors5.default.dim("\u2014 do not drop automatically. Confirm the org/environment is log-authoritative, then follow ") + import_picocolors5.default.bold("docs/runbooks/wal-stage5-customer-db-infra-cleanup.md") + import_picocolors5.default.dim(".\n")
280876
+ );
280877
+ process.exit(1);
280878
+ }
280676
280879
  async function connect(argv) {
280677
280880
  let args;
280678
280881
  try {
@@ -280685,18 +280888,28 @@ async function connect(argv) {
280685
280888
  await runCheck();
280686
280889
  return;
280687
280890
  }
280891
+ if (args.register) {
280892
+ await runRegister();
280893
+ return;
280894
+ }
280895
+ if (args.auditInfra) {
280896
+ await runAuditInfra();
280897
+ return;
280898
+ }
280688
280899
  printConnectRecipe(args);
280689
280900
  }
280690
- var CONNECT_USAGE = ` ablo connect \u2014 connect a real database to Ablo via logical replication (the one way)
280901
+ var CONNECT_USAGE = ` ablo connect \u2014 connect your database for the read path, via logical replication
280691
280902
 
280692
- Ablo READS your WAL and never runs DDL, owns, or migrates your schema. Your app
280693
- keeps writing through your own backend.
280903
+ The Electric/PowerSync/Zero model: Ablo consumes your WAL via logical replication and
280904
+ never runs DDL, owns, hosts, or migrates your schema. Your app continues to own the write path.
280694
280905
 
280695
280906
  Usage:
280696
280907
  npx ablo connect Print the exact setup SQL for your Postgres
280697
280908
  npx ablo connect --tables a,b,c Publish only these tables (default: all tables)
280698
280909
  npx ablo connect --role <name> Name the replication role (default: ablo_replicator)
280699
- npx ablo connect --check Validate DATABASE_URL is replication-ready`;
280910
+ npx ablo connect --check Validate DATABASE_URL is replication-ready
280911
+ npx ablo connect --register Register DATABASE_URL so Ablo replicates it (one self-service step)
280912
+ npx ablo connect --audit-infra Read-only Stage 5 audit for deprecated Ablo sync tables/types`;
280700
280913
 
280701
280914
  // src/cli/generate.ts
280702
280915
  init_cjs_shims();
@@ -280823,16 +281036,17 @@ function wireEnvLocal(apiKey, cwd = process.cwd()) {
280823
281036
  action = `Created ${import_picocolors7.default.bold(".env.local")} with ${import_picocolors7.default.bold("ABLO_API_KEY")}`;
280824
281037
  } else {
280825
281038
  const content = (0, import_fs7.readFileSync)(envPath, "utf8");
280826
- const match = content.match(/^ABLO_API_KEY=(.*)$/m);
281039
+ const match = /^ABLO_API_KEY=(.*)$/m.exec(content);
281040
+ const existing = match?.[1] ?? "";
280827
281041
  if (!match) {
280828
281042
  (0, import_fs7.appendFileSync)(envPath, `${content.endsWith("\n") || content.length === 0 ? "" : "\n"}${line}
280829
281043
  `);
280830
281044
  action = `Added ${import_picocolors7.default.bold("ABLO_API_KEY")} to ${import_picocolors7.default.bold(".env.local")}`;
280831
- } else if (match[1] === apiKey) {
281045
+ } else if (existing === apiKey) {
280832
281046
  action = `${import_picocolors7.default.bold(".env.local")} already has this key`;
280833
281047
  } else {
280834
281048
  (0, import_fs7.writeFileSync)(envPath, content.replace(/^ABLO_API_KEY=.*$/m, line));
280835
- action = `Updated ${import_picocolors7.default.bold("ABLO_API_KEY")} in ${import_picocolors7.default.bold(".env.local")} ${import_picocolors7.default.dim(`(was ${match[1].slice(0, 12)}\u2026)`)}`;
281049
+ action = `Updated ${import_picocolors7.default.bold("ABLO_API_KEY")} in ${import_picocolors7.default.bold(".env.local")} ${import_picocolors7.default.dim(`(was ${existing.slice(0, 12)}\u2026)`)}`;
280836
281050
  }
280837
281051
  }
280838
281052
  const gitignorePath = (0, import_path5.resolve)(cwd, ".gitignore");
@@ -280910,7 +281124,7 @@ async function dev(argv) {
280910
281124
  console.error(import_picocolors7.default.red(` ${err instanceof Error ? err.message : String(err)}`));
280911
281125
  process.exit(1);
280912
281126
  }
280913
- if (!args.apiKey) args.apiKey = resolveApiKey("sandbox");
281127
+ if (!args.apiKey) args.apiKey = resolveEffectiveApiKey("sandbox").key;
280914
281128
  const key = classifyKey(args.apiKey, getMode());
280915
281129
  if (!key.ok) {
280916
281130
  console.error(import_picocolors7.default.red(` ${key.reason}`));
@@ -281034,7 +281248,7 @@ async function deviceLogin(argv, deps = {}) {
281034
281248
  headers: { "content-type": "application/json" },
281035
281249
  body: JSON.stringify({ client_id: CLIENT_ID, scope: "openid profile email" })
281036
281250
  }).catch(() => null);
281037
- if (!codeRes || !codeRes.ok) {
281251
+ if (!codeRes?.ok) {
281038
281252
  M2.error(`Couldn't start login against ${AUTH_URL}. Is the dashboard reachable?`);
281039
281253
  process.exit(1);
281040
281254
  }
@@ -281104,7 +281318,7 @@ ${import_picocolors8.default.dim(url)}`, "Approve in your browser");
281104
281318
  ...targetProject ? { project_slug: targetProject } : {}
281105
281319
  })
281106
281320
  }).catch(() => null);
281107
- if (!provRes || !provRes.ok) {
281321
+ if (!provRes?.ok) {
281108
281322
  s.stop("Could not provision a key.");
281109
281323
  const reason = provRes ? (await provRes.json().catch(() => ({}))).error : void 0;
281110
281324
  if (reason) M2.error(reason);
@@ -281418,7 +281632,9 @@ function expiryLabel(iso) {
281418
281632
  }
281419
281633
  async function ping(apiUrl2) {
281420
281634
  const ctrl = new AbortController();
281421
- const t = setTimeout(() => ctrl.abort(), 3e3);
281635
+ const t = setTimeout(() => {
281636
+ ctrl.abort();
281637
+ }, 3e3);
281422
281638
  try {
281423
281639
  const res = await fetch(`${apiUrl2}/api/health`, { signal: ctrl.signal });
281424
281640
  return res.ok;
@@ -281431,7 +281647,9 @@ async function ping(apiUrl2) {
281431
281647
  async function fetchPushedSchema(apiUrl2, apiKey) {
281432
281648
  if (!apiKey) return null;
281433
281649
  const ctrl = new AbortController();
281434
- const t = setTimeout(() => ctrl.abort(), 3e3);
281650
+ const t = setTimeout(() => {
281651
+ ctrl.abort();
281652
+ }, 3e3);
281435
281653
  try {
281436
281654
  const res = await fetch(`${apiUrl2}/api/schema`, {
281437
281655
  headers: { authorization: `Bearer ${apiKey}` },
@@ -281445,6 +281663,47 @@ async function fetchPushedSchema(apiUrl2, apiKey) {
281445
281663
  clearTimeout(t);
281446
281664
  }
281447
281665
  }
281666
+ async function sampleRead(apiUrl2, apiKey, modelTypename, n) {
281667
+ const ctrl = new AbortController();
281668
+ const t = setTimeout(() => {
281669
+ ctrl.abort();
281670
+ }, 4e3);
281671
+ try {
281672
+ const res = await fetch(
281673
+ `${apiUrl2}/v1/models/${encodeURIComponent(modelTypename)}/__ablo_health_probe_${n}__`,
281674
+ { headers: { authorization: `Bearer ${apiKey}` }, signal: ctrl.signal }
281675
+ );
281676
+ if (res.ok) return "routed";
281677
+ let code;
281678
+ try {
281679
+ const body = await res.json();
281680
+ code = body.code ?? body.error?.code;
281681
+ } catch {
281682
+ }
281683
+ if (code === "entity_not_found") return "routed";
281684
+ if (code === "tenant_routing_failed") return "no_route";
281685
+ if (res.status === 401 || res.status === 403) return { forbidden: code };
281686
+ return { other: `${res.status}${code ? ` ${code}` : ""}` };
281687
+ } catch {
281688
+ return { other: "unreachable" };
281689
+ } finally {
281690
+ clearTimeout(t);
281691
+ }
281692
+ }
281693
+ async function probeDataPlane(apiUrl2, apiKey, modelTypename) {
281694
+ if (!apiKey) return { status: "skipped" };
281695
+ const samples = [];
281696
+ for (let i = 0; i < 3; i++) samples.push(await sampleRead(apiUrl2, apiKey, modelTypename, i));
281697
+ const forbidden = samples.find((s) => typeof s === "object" && "forbidden" in s);
281698
+ if (forbidden) return { status: "forbidden", detail: forbidden.forbidden };
281699
+ const routed = samples.filter((s) => s === "routed").length;
281700
+ const noRoute = samples.filter((s) => s === "no_route").length;
281701
+ if (routed === samples.length) return { status: "ok" };
281702
+ if (noRoute === samples.length) return { status: "no_database" };
281703
+ if (routed > 0 && noRoute > 0) return { status: "intermittent", ok: routed, failed: noRoute };
281704
+ const other = samples.find((s) => typeof s === "object" && "other" in s);
281705
+ return { status: "unknown", detail: other?.other ?? "inconclusive" };
281706
+ }
281448
281707
  function formatConflict(conflict) {
281449
281708
  if (!conflict) return "";
281450
281709
  const parts = ["user", "agent", "system"].flatMap((k3) => conflict[k3] ? [`${k3}:${conflict[k3]}`] : []);
@@ -281459,12 +281718,19 @@ async function status(args = []) {
281459
281718
  const key2 = describeEffectiveKey(mode2, process.env.ABLO_API_KEY, entry);
281460
281719
  const plan2 = resolvePushPlan();
281461
281720
  const activeProject2 = getActiveProject();
281462
- const introspectKey = process.env.ABLO_API_KEY ?? entry?.apiKey;
281463
- const pushed = await fetchPushedSchema(apiUrl2, introspectKey);
281721
+ const effective2 = resolveEffectiveApiKey();
281722
+ const pushed = await fetchPushedSchema(apiUrl2, effective2.key);
281464
281723
  const out = {
281465
281724
  mode: mode2,
281466
281725
  // The locally-active project (`ablo projects use`); null = org-default.
281467
281726
  project: activeProject2 ?? null,
281727
+ // The credential the CLI resolves for requests, with its provenance —
281728
+ // 'env' | '.env.local' | '.env' | 'stored' (users debug key confusion
281729
+ // constantly; the SOURCE is usually the answer).
281730
+ effectiveKey: {
281731
+ prefix: effective2.key ? effective2.key.slice(0, 12) : null,
281732
+ source: effective2.source
281733
+ },
281468
281734
  keyPrefix: key2.keyPrefix,
281469
281735
  keySource: key2.keySource,
281470
281736
  keyMode: key2.keyMode,
@@ -281498,9 +281764,11 @@ async function status(args = []) {
281498
281764
  console.log(`
281499
281765
  ${brand("ablo")} ${import_picocolors11.default.dim("status")}
281500
281766
  `);
281501
- if (process.env.ABLO_API_KEY) {
281767
+ const effective = resolveEffectiveApiKey();
281768
+ if (effective.key && effective.source && effective.source !== "stored") {
281769
+ const label = effective.source === "env" ? "ABLO_API_KEY env" : effective.source;
281502
281770
  console.log(
281503
- ` ${import_picocolors11.default.dim("key")} ${process.env.ABLO_API_KEY.slice(0, 12)}\u2026 ${import_picocolors11.default.dim("(ABLO_API_KEY env \u2014 overrides stored)")}`
281771
+ ` ${import_picocolors11.default.dim("key")} ${effective.key.slice(0, 12)}\u2026 ${import_picocolors11.default.dim(`(${label} \u2014 overrides stored)`)}`
281504
281772
  );
281505
281773
  } else if (!cfg) {
281506
281774
  console.log(` ${import_picocolors11.default.yellow("!")} Not logged in \u2014 run ${import_picocolors11.default.bold("ablo login")}.`);
@@ -281535,9 +281803,9 @@ async function status(args = []) {
281535
281803
  const reachable = await ping(apiUrl2);
281536
281804
  console.log(reachable ? import_picocolors11.default.green("reachable") : import_picocolors11.default.red("unreachable"));
281537
281805
  if (reachable) {
281538
- const introspectKey = process.env.ABLO_API_KEY ?? activeEntry?.apiKey;
281806
+ const introspectKey = effective.key;
281539
281807
  const pushed = await fetchPushedSchema(apiUrl2, introspectKey);
281540
- if (pushed && pushed.active) {
281808
+ if (pushed?.active) {
281541
281809
  const when = pushed.pushedAt ? ` ${import_picocolors11.default.dim(`@ ${pushed.pushedAt.slice(0, 10)}`)}` : "";
281542
281810
  const ver = pushed.version != null ? ` ${import_picocolors11.default.dim(`(rev ${pushed.version})`)}` : "";
281543
281811
  console.log(` ${import_picocolors11.default.dim("schema")} ${import_picocolors11.default.bold(`${pushed.models.length} models pushed`)}${ver}${when}`);
@@ -281550,6 +281818,29 @@ async function status(args = []) {
281550
281818
  } else if (pushed && !pushed.active) {
281551
281819
  console.log(` ${import_picocolors11.default.dim("schema")} ${import_picocolors11.default.yellow("none pushed")} ${import_picocolors11.default.dim(`(run ${import_picocolors11.default.bold("ablo push")} or ${import_picocolors11.default.bold("ablo dev")})`)}`);
281552
281820
  }
281821
+ const firstPushedModel = pushed?.active ? pushed.models[0] : void 0;
281822
+ if (firstPushedModel !== void 0) {
281823
+ const probe = await probeDataPlane(apiUrl2, introspectKey, firstPushedModel.typename);
281824
+ if (probe.status === "no_database") {
281825
+ console.log(` ${import_picocolors11.default.dim("data")} ${import_picocolors11.default.red("\u2717 no database registered")}${org ? import_picocolors11.default.dim(` for org ${org}`) : ""}`);
281826
+ console.log(
281827
+ ` ${import_picocolors11.default.dim(
281828
+ `reads/writes will fail with ${import_picocolors11.default.bold("tenant_routing_failed")}. Connect one with ${import_picocolors11.default.bold("ablo connect")}, or point ${import_picocolors11.default.bold("ABLO_API_KEY")} at an org that has a database.`
281829
+ )}`
281830
+ );
281831
+ } else if (probe.status === "intermittent") {
281832
+ console.log(` ${import_picocolors11.default.dim("data")} ${import_picocolors11.default.red(`\u2717 database routing is intermittent`)} ${import_picocolors11.default.dim(`(${probe.ok} ok / ${probe.failed} failed of ${probe.ok + probe.failed})`)}${org ? import_picocolors11.default.dim(` for org ${org}`) : ""}`);
281833
+ console.log(
281834
+ ` ${import_picocolors11.default.dim(
281835
+ `some reads/writes fail with ${import_picocolors11.default.bold("tenant_routing_failed")} \u2014 the registration is unstable. Re-establish it with ${import_picocolors11.default.bold("ablo connect")} (or check for a recent server redeploy).`
281836
+ )}`
281837
+ );
281838
+ } else if (probe.status === "forbidden") {
281839
+ console.log(` ${import_picocolors11.default.dim("data")} ${import_picocolors11.default.yellow("? key not authorized to read")}${probe.detail ? import_picocolors11.default.dim(` (${probe.detail})`) : ""}`);
281840
+ } else if (probe.status === "unknown") {
281841
+ console.log(` ${import_picocolors11.default.dim("data")} ${import_picocolors11.default.yellow(`? data-plane check inconclusive (${probe.detail})`)}`);
281842
+ }
281843
+ }
281553
281844
  }
281554
281845
  console.log();
281555
281846
  }
@@ -281720,8 +282011,10 @@ function parseMode(args) {
281720
282011
  function positional(args) {
281721
282012
  for (let i = 0; i < args.length; i++) {
281722
282013
  const a = args[i];
282014
+ if (a === void 0) continue;
281723
282015
  if (a.startsWith("-")) {
281724
- if (!a.includes("=") && args[i + 1] && !args[i + 1].startsWith("-")) i++;
282016
+ const next = args[i + 1];
282017
+ if (!a.includes("=") && next && !next.startsWith("-")) i++;
281725
282018
  continue;
281726
282019
  }
281727
282020
  return a;
@@ -282042,8 +282335,8 @@ function verbRewrite(call, verb) {
282042
282335
  const params = VERB_ARGS[verb];
282043
282336
  if (!params) return null;
282044
282337
  const args = call.getArguments();
282045
- if (args.length === 0) return null;
282046
282338
  const first = args[0];
282339
+ if (first === void 0) return null;
282047
282340
  const calleeText = call.getExpression().getText();
282048
282341
  if (verb === "create") {
282049
282342
  if (import_ts_morph.Node.isObjectLiteralExpression(first) && hasKey2(first, "data")) return null;
@@ -282092,7 +282385,7 @@ async function upgrade(argv) {
282092
282385
  edits.push({ file, line: node.getStartLineNumber(), rule, before, after });
282093
282386
  };
282094
282387
  const flag2 = (node, rule, hint) => {
282095
- manual.push({ file, line: node.getStartLineNumber(), rule, snippet: node.getText().split("\n")[0].slice(0, 120), hint });
282388
+ manual.push({ file, line: node.getStartLineNumber(), rule, snippet: (node.getText().split("\n")[0] ?? "").slice(0, 120), hint });
282096
282389
  };
282097
282390
  for (const imp of sf.getImportDeclarations()) {
282098
282391
  if (!ABLO_REACT.has(imp.getModuleSpecifierValue())) continue;
@@ -282122,13 +282415,13 @@ async function upgrade(argv) {
282122
282415
  if (method in VERB_ARGS) {
282123
282416
  const next = verbRewrite(call, method);
282124
282417
  if (next) {
282125
- record(call, `${method}\u2192object-param`, call.getText().split("\n")[0].slice(0, 80), next.slice(0, 80));
282418
+ record(call, `${method}\u2192object-param`, (call.getText().split("\n")[0] ?? "").slice(0, 80), next.slice(0, 80));
282126
282419
  call.replaceWithText(next);
282127
282420
  }
282128
282421
  } else if (method === "load") {
282129
282422
  const next = loadRewrite(call);
282130
282423
  if (next) {
282131
- record(call, "load\u2192retrieve/list", call.getText().split("\n")[0].slice(0, 80), next.slice(0, 80));
282424
+ record(call, "load\u2192retrieve/list", (call.getText().split("\n")[0] ?? "").slice(0, 80), next.slice(0, 80));
282132
282425
  call.replaceWithText(next);
282133
282426
  }
282134
282427
  }
@@ -282438,7 +282731,7 @@ function stripComments(src) {
282438
282731
  let inStr = false;
282439
282732
  let res = "";
282440
282733
  for (let i = 0; i < line.length; i++) {
282441
- const c = line[i];
282734
+ const c = line.charAt(i);
282442
282735
  if (c === '"') {
282443
282736
  inStr = !inStr;
282444
282737
  res += c;
@@ -282466,8 +282759,10 @@ function parseBlocks(srcRaw) {
282466
282759
  }
282467
282760
  const body = src.slice(openIdx + 1, j2 - 1);
282468
282761
  const kw = m2[1];
282762
+ const name = m2[2];
282763
+ if (name === void 0) throw new Error("unparseable block header in the Prisma schema");
282469
282764
  const type = kw === "model" ? "model" : kw === "enum" ? "enum" : "other";
282470
- blocks.push({ type, name: m2[2], body });
282765
+ blocks.push({ type, name, body });
282471
282766
  headerRe.lastIndex = j2;
282472
282767
  }
282473
282768
  return blocks;
@@ -282478,7 +282773,7 @@ function parseEnumMembers(body) {
282478
282773
  const t = line.trim();
282479
282774
  if (!t || t.startsWith("@") || t.startsWith("@@")) continue;
282480
282775
  const first = t.split(/\s+/)[0];
282481
- if (/^\w+$/.test(first)) members.push(first);
282776
+ if (first !== void 0 && /^\w+$/.test(first)) members.push(first);
282482
282777
  }
282483
282778
  return members;
282484
282779
  }
@@ -282487,9 +282782,11 @@ function parseFieldLine(line) {
282487
282782
  if (!t || t.startsWith("@@") || t.startsWith("//")) return null;
282488
282783
  const m2 = /^(\w+)\s+([A-Za-z_]\w*)(\[\])?(\?)?\s*(.*)$/.exec(t);
282489
282784
  if (!m2) return null;
282785
+ const [, name, type] = m2;
282786
+ if (name === void 0 || type === void 0) return null;
282490
282787
  return {
282491
- name: m2[1],
282492
- type: m2[2],
282788
+ name,
282789
+ type,
282493
282790
  list: Boolean(m2[3]),
282494
282791
  optional: Boolean(m2[4]),
282495
282792
  attrs: m2[5] ?? ""
@@ -282502,11 +282799,11 @@ function blockMap(body) {
282502
282799
  return /@@map\("([^"]+)"\)/.exec(body)?.[1];
282503
282800
  }
282504
282801
  function relationFkFields(attrs) {
282505
- const rel = /@relation\(([^)]*)\)/.exec(attrs);
282506
- if (!rel) return null;
282507
- const fields = /fields:\s*\[([^\]]*)\]/.exec(rel[1]);
282508
- if (!fields) return null;
282509
- return fields[1].split(",").map((s) => s.trim()).filter(Boolean);
282802
+ const rel = /@relation\(([^)]*)\)/.exec(attrs)?.[1];
282803
+ if (rel === void 0) return null;
282804
+ const fields = /fields:\s*\[([^\]]*)\]/.exec(rel)?.[1];
282805
+ if (fields === void 0) return null;
282806
+ return fields.split(",").map((s) => s.trim()).filter(Boolean);
282510
282807
  }
282511
282808
  function parsePrismaSchema(src) {
282512
282809
  const blocks = parseBlocks(src);
@@ -282531,8 +282828,9 @@ function parsePrismaSchema(src) {
282531
282828
  const column = mapAttr(raw.attrs);
282532
282829
  if (modelNames.has(raw.type)) {
282533
282830
  const fks = relationFkFields(raw.attrs);
282534
- if (fks && fks.length === 1) {
282535
- relations.push({ name: raw.name, target: tableOf.get(raw.type) ?? raw.type, fkField: fks[0] });
282831
+ const fkField = fks && fks.length === 1 ? fks[0] : void 0;
282832
+ if (fkField !== void 0) {
282833
+ relations.push({ name: raw.name, target: tableOf.get(raw.type) ?? raw.type, fkField });
282536
282834
  }
282537
282835
  continue;
282538
282836
  }
@@ -282595,6 +282893,7 @@ function parsePrismaPullArgs(argv) {
282595
282893
  let force = false;
282596
282894
  for (let i = 0; i < argv.length; i++) {
282597
282895
  const arg = argv[i];
282896
+ if (arg === void 0) continue;
282598
282897
  switch (arg) {
282599
282898
  case "--schema":
282600
282899
  schema = argv[++i] ?? schema;
@@ -282744,8 +283043,8 @@ async function lowerDrizzleModule(mod) {
282744
283043
  const { foreignKeys } = getTableConfig(table);
282745
283044
  for (const fk of foreignKeys) {
282746
283045
  const ref = fk.reference();
282747
- if (ref.columns.length !== 1) continue;
282748
- const localCol = ref.columns[0];
283046
+ const [localCol, ...extraCols] = ref.columns;
283047
+ if (localCol === void 0 || extraCols.length > 0) continue;
282749
283048
  const fkField = fieldKeyByColumn.get(localCol) ?? fieldKeyByColName.get(localCol.name) ?? localCol.name;
282750
283049
  const target = getTableName(ref.foreignTable);
282751
283050
  relations.push({ name: stripIdSuffix(fkField), target, fkField });
@@ -282773,6 +283072,7 @@ function parseDrizzlePullArgs(argv) {
282773
283072
  let force = false;
282774
283073
  for (let i = 0; i < argv.length; i++) {
282775
283074
  const arg = argv[i];
283075
+ if (arg === void 0) continue;
282776
283076
  switch (arg) {
282777
283077
  case "--schema":
282778
283078
  schema = argv[++i] ?? schema;
@@ -282832,7 +283132,7 @@ async function drizzlePull(argv) {
282832
283132
  result = await buildSchemaSourceFromDrizzle({ mod, importPath: args.importPath });
282833
283133
  } catch (err) {
282834
283134
  const msg = err instanceof Error ? err.message : String(err);
282835
- const hint = /Cannot find package 'drizzle-orm'/.test(msg) ? import_picocolors18.default.dim(` (install ${import_picocolors18.default.bold("drizzle-orm")} in this project)`) : "";
283135
+ const hint = msg.includes("Cannot find package 'drizzle-orm'") ? import_picocolors18.default.dim(` (install ${import_picocolors18.default.bold("drizzle-orm")} in this project)`) : "";
282836
283136
  console.error(import_picocolors18.default.red(` Couldn't load the schema: ${msg}`) + hint);
282837
283137
  process.exit(1);
282838
283138
  }
@@ -282864,7 +283164,7 @@ var RECOVERY_HINT = {
282864
283164
  permission: "Your key isn't allowed to do this \u2014 check its scopes or role.",
282865
283165
  session_expiry: "Your session expired \u2014 sign in again.",
282866
283166
  access_credential_expiry: "Your access credential expired \u2014 refresh it and retry.",
282867
- auth_blocked: "Authentication was blocked."
283167
+ auth_blocked: "Authentication was blocked \u2014 the credential itself was rejected. Check that the key matches this environment."
282868
283168
  };
282869
283169
  function titleForType(type) {
282870
283170
  const core = type.replace(/^Ablo/, "").replace(/Error$/, "");
@@ -282900,7 +283200,9 @@ function renderKnownDetails(details, line) {
282900
283200
  }
282901
283201
  }
282902
283202
  function renderCliError(err, opts = {}) {
282903
- const line = opts.write ?? ((l2) => console.error(l2));
283203
+ const line = opts.write ?? ((l2) => {
283204
+ console.error(l2);
283205
+ });
282904
283206
  const verbose = opts.verbose ?? (process.argv.includes("--verbose") || process.env.ABLO_VERBOSE === "1");
282905
283207
  if (err instanceof AbloError) {
282906
283208
  const codeTag = err.code ? ` ${import_picocolors19.default.dim(`[${err.code}]`)}` : "";
@@ -283055,6 +283357,7 @@ async function main() {
283055
283357
  console.log(` npx ablo check Check your existing database fits the schema (read-only, creates no tables)`);
283056
283358
  console.log(` npx ablo connect Connect a real database \u2014 prints the logical-replication setup SQL (the one way)`);
283057
283359
  console.log(` npx ablo connect --check Validate DATABASE_URL is replication-ready (wal_level, publication, role, replica identity)`);
283360
+ console.log(` npx ablo connect --audit-infra Read-only audit for deprecated Ablo sync infra in a customer DB`);
283058
283361
  console.log(` npx ablo migrate Provision your synced-model tables in your own Postgres (optional escape hatch \u2014 \`connect\` is the way)`);
283059
283362
  console.log(` npx ablo migrate --dry-run Print the SQL without executing (preview)`);
283060
283363
  console.log(` npx ablo push Upload your schema definition to Ablo (metadata only \u2014 rows stay in your DB)`);
@@ -283080,7 +283383,7 @@ function bailIfCancelled(value) {
283080
283383
  }
283081
283384
  var INIT_FRAMEWORKS = ["nextjs", "vite", "remix", "vanilla"];
283082
283385
  var INIT_AUTHS = ["apikey", "firebase", "auth0", "clerk", "supabase", "betterauth", "jwt"];
283083
- var INIT_STORAGES = ["endpoint", "direct", "datasource"];
283386
+ var INIT_STORAGES = ["replication", "endpoint", "direct", "datasource"];
283084
283387
  function parseInitArgs(args) {
283085
283388
  const has = (flag2) => args.includes(flag2);
283086
283389
  const val = (flag2) => {
@@ -283128,7 +283431,7 @@ function detectOrm(override) {
283128
283431
  try {
283129
283432
  const pkg = JSON.parse((0, import_fs12.readFileSync)("package.json", "utf-8"));
283130
283433
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
283131
- if (deps["@prisma/client"] || deps["prisma"]) return "prisma";
283434
+ if (deps["@prisma/client"] || deps.prisma) return "prisma";
283132
283435
  if (deps["drizzle-orm"]) return "drizzle";
283133
283436
  } catch {
283134
283437
  }
@@ -283206,15 +283509,15 @@ async function init(args = []) {
283206
283509
  const storageChoice = await chooseOption(
283207
283510
  "storage",
283208
283511
  opts.storage,
283209
- "endpoint",
283512
+ "replication",
283210
283513
  INIT_STORAGES,
283211
283514
  false,
283212
- () => Promise.resolve("endpoint")
283515
+ () => Promise.resolve("replication")
283213
283516
  );
283214
283517
  const storage = storageChoice === "datasource" ? "endpoint" : storageChoice;
283215
283518
  if (storage === "direct") {
283216
283519
  Me(
283217
- "`--storage direct` uses the deprecated databaseUrl connector. The signed Data\nSource endpoint is the supported path.",
283520
+ "`--storage direct` uses the deprecated databaseUrl connector. Logical replication\n(`npx ablo connect`) is the supported path; a signed Data Source endpoint is the fallback.",
283218
283521
  import_picocolors20.default.yellow("Deprecated")
283219
283522
  );
283220
283523
  }
@@ -283335,7 +283638,11 @@ async function init(args = []) {
283335
283638
  `Run ${import_picocolors20.default.bold("npx ablo login")} (or add ${import_picocolors20.default.bold("ABLO_API_KEY")} to ${import_picocolors20.default.bold(envFile)})`,
283336
283639
  `Set ${import_picocolors20.default.bold("DATABASE_URL")} in ${import_picocolors20.default.bold(envFile)} \u2014 your Postgres is the system of record; rows live there, never with Ablo`,
283337
283640
  `Run ${import_picocolors20.default.bold("npx ablo dev")} \u2014 pushes your schema definition and watches for changes`,
283338
- ...storage === "direct" ? [
283641
+ ...storage === "replication" ? [
283642
+ `Connect your database \u2014 ${import_picocolors20.default.bold("npx ablo connect")} prints the one-time logical-replication setup SQL to run on your Postgres`,
283643
+ `Verify it \u2014 ${import_picocolors20.default.bold("npx ablo connect --check")} walks wal_level, the publication, the role, and replica identity, with the exact fix for anything missing`,
283644
+ `Register it \u2014 ${import_picocolors20.default.bold("npx ablo connect --register")} tells Ablo to start replicating; your app keeps writing through your own backend while Ablo tails the WAL`
283645
+ ] : storage === "direct" ? [
283339
283646
  `Provision your DB: ${import_picocolors20.default.bold("npx ablo migrate")} (creates your synced-model tables with row-level security; keep your own migrations for everything else)`
283340
283647
  ] : [
283341
283648
  `Provision your DB: ${import_picocolors20.default.bold("npx ablo migrate")} (creates your Ablo-model tables + the adapter tables; keep your own migrations for everything else), then mount ${import_picocolors20.default.bold(`${abloDir}/data-source.ts`)} at ${import_picocolors20.default.bold("/api/ablo/source")}`
@@ -283440,7 +283747,7 @@ export {};
283440
283747
  }
283441
283748
  function generateEnv(storage, opts = {}) {
283442
283749
  const { includeApiKey = true } = opts;
283443
- const databaseBlock = storage === "direct" ? "# DEPRECATED direct connector. The client registers this connection (sent once\n# over TLS, stored sealed) so Ablo dials into your Postgres. Prefer a signed Data\n# Source endpoint; to keep the transaction log in your infra too, self-host the engine.\n# Use a dedicated non-superuser role; the browser never sees this value.\nDATABASE_URL=postgres://user:password@host:5432/db\n" : "# Used by ablo/data-source.ts (your DB endpoint) + `ablo migrate` \u2014 NOT the client.\n# Ablo never sees it; the browser never sees it. Your DB stays in your app.\nDATABASE_URL=postgres://user:password@host:5432/db\n";
283750
+ const databaseBlock = storage === "direct" ? "# DEPRECATED direct connector. The client registers this connection (sent once\n# over TLS, stored sealed) so Ablo dials into your Postgres. Prefer logical\n# replication (`npx ablo connect`); to keep the transaction log in your infra too,\n# self-host the engine. Use a dedicated non-superuser role; the browser never sees this.\nDATABASE_URL=postgres://user:password@host:5432/db\n" : storage === "replication" ? "# Used by `npx ablo connect` to set up + register logical replication \u2014 the\n# DIRECT (un-pooled) endpoint. Ablo TAILS your WAL from here; it never writes.\n# The client never sees it; the browser never sees it. Your DB stays yours.\nDATABASE_URL=postgres://user:password@host:5432/db\n" : "# Used by ablo/data-source.ts (your DB endpoint) + `ablo migrate` \u2014 NOT the client.\n# Ablo never sees it; the browser never sees it. Your DB stays in your app.\nDATABASE_URL=postgres://user:password@host:5432/db\n";
283444
283751
  const webhookBlock = storage === "endpoint" ? "# Signing secret for the webhook receiver (app/api/ablo/webhooks/route.ts).\n# Ablo mints this when you register the endpoint's URL (POST /v1/webhook_endpoints\n# or the dashboard) and returns it once \u2014 paste it here.\nABLO_WEBHOOK_SECRET=whsec_your_endpoint_secret_here\n" : "";
283445
283752
  const apiKeyBlock = includeApiKey ? "# Ablo Sync Engine \u2014 use a sk_test_ key for local dev (`npx ablo push`)\nABLO_API_KEY=sk_test_your_key_here\n" : "";
283446
283753
  return `${apiKeyBlock}${webhookBlock}${databaseBlock}`;
@@ -283707,23 +284014,12 @@ import Ablo from '@abloatai/ablo';
283707
284014
  import { AbloProvider } from '@abloatai/ablo/react';
283708
284015
  import { schema } from '@/ablo/schema';
283709
284016
 
283710
- // The browser client holds NO secret. The \`apiKey\` resolver fetches the route
284017
+ // The browser client holds NO secret. The endpoint string points at the route
283711
284018
  // below, which mints a short-lived session token (already scoped to the org +
283712
- // user); the client keeps it fresh (refresh timer + wake/online/focus re-mint).
283713
- // Contract: return the token, return \`null\` when the user is signed out
283714
- // (\u2192 the client signs out), or throw on a transient failure (\u2192 it retries).
283715
- const ablo = Ablo({
283716
- schema,
283717
- apiKey: async () => {
283718
- const res = await fetch('/api/ablo-session', {
283719
- method: 'POST',
283720
- credentials: 'include',
283721
- });
283722
- if (!res.ok) return null;
283723
- const { token } = (await res.json()) as { token: string | null };
283724
- return token;
283725
- },
283726
- });
284019
+ // user); the SDK owns the exchange \u2014 it fetches the route (POST, same-origin
284020
+ // cookies) and keeps the token fresh (refresh timer + wake re-mint). A 401
284021
+ // from the route signs the user out; any other failure is retried.
284022
+ const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
283727
284023
 
283728
284024
  export function Providers({ children }: { children: React.ReactNode }) {
283729
284025
  return <AbloProvider client={ablo}>{children}</AbloProvider>;
@@ -283744,8 +284040,8 @@ export async function POST(): Promise<Response> {
283744
284040
  const user = await getCurrentUser();
283745
284041
  if (!user) return new Response('Unauthorized', { status: 401 });
283746
284042
 
283747
- const { token } = await sync.sessions.create({ user: { id: user.id } });
283748
- return Response.json({ token });
284043
+ const { token, expiresAt } = await sync.sessions.create({ user: { id: user.id } });
284044
+ return Response.json({ token, expiresAt });
283749
284045
  }
283750
284046
 
283751
284047
  // Validate the signed-in user SERVER-SIDE. This ships wired for Better Auth;