@abloatai/ablo 0.25.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 (244) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +3 -3
  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 +328 -262
  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/examples/existing-python-backend.md +3 -3
  220. package/docs/identity.md +4 -4
  221. package/docs/integration-guide.md +1 -1
  222. package/docs/react.md +1 -1
  223. package/docs/sessions.md +5 -7
  224. package/package.json +15 -13
  225. package/dist/client/index.d.ts +0 -36
  226. package/dist/client/index.js +0 -33
  227. package/dist/config/index.d.ts +0 -10
  228. package/dist/config/index.js +0 -12
  229. package/dist/interfaces/headless.d.ts +0 -95
  230. package/dist/interfaces/headless.js +0 -41
  231. package/dist/query/index.d.ts +0 -6
  232. package/dist/query/index.js +0 -5
  233. package/dist/realtime/index.d.ts +0 -10
  234. package/dist/realtime/index.js +0 -9
  235. package/dist/schema/plane.d.ts +0 -23
  236. package/dist/schema/plane.js +0 -19
  237. package/dist/server/next.d.ts +0 -51
  238. package/dist/server/next.js +0 -47
  239. package/dist/sync/OfflineFlush.d.ts +0 -9
  240. package/dist/sync/OfflineFlush.js +0 -22
  241. package/dist/sync/OfflineTransactionStore.d.ts +0 -37
  242. package/dist/sync/OfflineTransactionStore.js +0 -263
  243. package/dist/transactions/index.d.ts +0 -16
  244. package/dist/transactions/index.js +0 -7
@@ -25,7 +25,7 @@ import { scopeSchema, grantsRefSchema } from './roles.js';
25
25
  // Sync-group roles (identity + entity) live in `./roles.js`. Re-exported here
26
26
  // so the long-standing `@ablo/schema` / `./schema.js` import paths keep working
27
27
  // after the rehome — see roles.ts for the full vocabulary.
28
- export { identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, } from './roles.js';
28
+ export { identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, intersectRequestedWithAllowed, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, } from './roles.js';
29
29
  function resolveCasing(fn) {
30
30
  if (fn === undefined)
31
31
  return (x) => x;
@@ -108,17 +108,18 @@ function assertRoundTrippableCamelCase(modelName, fieldName) {
108
108
  return;
109
109
  // Leading-lowercase constraint: fields must be camelCase, not PascalCase.
110
110
  // PascalCase is reserved for typenames.
111
- if (fieldName[0] >= 'A' && fieldName[0] <= 'Z') {
111
+ const first = fieldName.charAt(0);
112
+ if (first >= 'A' && first <= 'Z') {
112
113
  throw new AbloValidationError(`[defineSchema] ${modelName}.${fieldName}: field names must start lowercase ` +
113
- `(camelCase). Use "${fieldName[0].toLowerCase()}${fieldName.slice(1)}" instead.`, { code: 'schema_field_not_camelcase' });
114
+ `(camelCase). Use "${first.toLowerCase()}${fieldName.slice(1)}" instead.`, { code: 'schema_field_not_camelcase' });
114
115
  }
115
116
  // Two-consecutive-uppercase check. The classic failure mode is
116
117
  // `contentJSON`, `contentHTML`, `myURLParam`, etc. These don't round-trip
117
118
  // through `postgres.camel` — the snake_case intermediate would be
118
119
  // `content_j_s_o_n`, which is not a column that exists.
119
120
  for (let i = 0; i < fieldName.length - 1; i++) {
120
- const a = fieldName[i];
121
- const b = fieldName[i + 1];
121
+ const a = fieldName.charAt(i);
122
+ const b = fieldName.charAt(i + 1);
122
123
  const aUpper = a >= 'A' && a <= 'Z';
123
124
  const bUpper = b >= 'A' && b <= 'Z';
124
125
  if (aUpper && bUpper) {
@@ -184,8 +185,7 @@ export function defineSchema(models, options) {
184
185
  // columns stay declared in the artifact instead of rediscovered by
185
186
  // SQL compilers. Server-side SQL compilers read the resolved value
186
187
  // directly.
187
- for (const relName of Object.keys(def.relations)) {
188
- const rel = def.relations[relName];
188
+ for (const rel of Object.values(def.relations)) {
189
189
  const fieldColumn = def.fields[rel.foreignKey]?.column;
190
190
  rel.foreignKeyColumn = fieldColumn ?? casing(rel.foreignKey);
191
191
  }
@@ -245,6 +245,10 @@ function validateSyncGroupSchema(models) {
245
245
  // The scope edge must target a model that is actually a scope root —
246
246
  // otherwise the resolved `<kind>:<id>` group is one nothing fans into.
247
247
  const scopeRel = relations[def.grants.scope];
248
+ // Unreachable: the role loop above already threw if the scope relation
249
+ // is missing — this narrows the indexed access for the checks below.
250
+ if (!scopeRel)
251
+ continue;
248
252
  const target = models[scopeRel.target];
249
253
  if (target && !target.scope) {
250
254
  throw new AbloValidationError(`Model "${name}": grants.scope "${def.grants.scope}" targets "${scopeRel.target}", ` +
@@ -30,10 +30,10 @@
30
30
  */
31
31
  import type { FieldMeta } from './field.js';
32
32
  import type { Tenancy } from './tenancy.js';
33
- import type { SchemaPlane } from './plane.js';
33
+ import type { ModelResidency } from './residency.js';
34
34
  import type { GrantsRef, LoadStrategy, PersistOptions, AutoFillRule, ConflictAxis } from './model.js';
35
35
  import type { RelationType } from './relation.js';
36
- import { type Schema, type SchemaRecord, type IdentityRole, type EntityRole } from './schema.js';
36
+ import { type Schema, type IdentityRole, type EntityRole } from './schema.js';
37
37
  /** Current schema-JSON envelope version. Bump on a breaking change to the
38
38
  * JSON shape itself (not the user's schema). v2 replaced the per-model
39
39
  * `syncGroupFormat` template string with structured `scope`/`grants`/
@@ -58,7 +58,7 @@ export interface ModelJSON {
58
58
  readonly tenancy: Tenancy;
59
59
  /** Database plane. Optional for back-compat: absent in artifacts written before
60
60
  * the plane axis → read as `tenant` (the default). See `./plane.ts`. */
61
- readonly plane?: SchemaPlane;
61
+ readonly plane?: ModelResidency;
62
62
  readonly scope?: boolean | string;
63
63
  readonly grants?: GrantsRef;
64
64
  readonly entityRoles?: readonly EntityRole[];
@@ -84,18 +84,18 @@ export interface SchemaJSON {
84
84
  * (validators, `computed`); keeps everything the server and a faithful
85
85
  * rebuild need. The result is plain data — `JSON.stringify`-safe.
86
86
  */
87
- export declare function toSchemaJSON(schema: Schema<SchemaRecord>): SchemaJSON;
87
+ export declare function toSchemaJSON(schema: Schema): SchemaJSON;
88
88
  /** Serialize a `Schema` to a JSON string (the `ablo push` payload). */
89
- export declare function serializeSchema(schema: Schema<SchemaRecord>): string;
89
+ export declare function serializeSchema(schema: Schema): string;
90
90
  /**
91
91
  * Reconstruct a working `Schema` from its JSON form. Validators are rebuilt
92
92
  * permissively from field metadata (the server never validates field shapes);
93
93
  * `computed` getters are absent. Everything the server reads — routing,
94
94
  * scoping, relations, identity roles — is restored exactly.
95
95
  */
96
- export declare function fromSchemaJSON(json: SchemaJSON): Schema<SchemaRecord>;
96
+ export declare function fromSchemaJSON(json: SchemaJSON): Schema;
97
97
  /** Parse a `Schema` from a JSON string (inverse of {@link serializeSchema}). */
98
- export declare function parseSchema(json: string): Schema<SchemaRecord>;
98
+ export declare function parseSchema(json: string): Schema;
99
99
  /**
100
100
  * Stable content hash of a `Schema`'s JSON form. FNV-1a over a canonical
101
101
  * (sorted-key) encoding — deterministic across runs and order-invariant, no
@@ -103,5 +103,5 @@ export declare function parseSchema(json: string): Schema<SchemaRecord>;
103
103
  * the hash it was built against, the server compares it to the tenant's
104
104
  * active schema hash. Not a security primitive.
105
105
  */
106
- export declare function schemaHash(schema: Schema<SchemaRecord>): string;
106
+ export declare function schemaHash(schema: Schema): string;
107
107
  export {};
@@ -17,7 +17,7 @@
17
17
  * - {@link deltaProvenanceSchema} — which AI task caused it. `control` plane.
18
18
  *
19
19
  * {@link syncDeltaRowSchema} is the full stored row (core ∪ attribution ∪
20
- * provenance). {@link DELTA_PLANES} declares each slice's plane so provisioning
20
+ * provenance). {@link DELTA_RESIDENCY} declares each slice's plane so provisioning
21
21
  * (P1) can derive "what a customer DB gets" from the schema, not hand-code it.
22
22
  *
23
23
  * Distinct from the WIRE `SyncDelta` (`sync/SyncWebSocket.ts`, client-facing) and
@@ -152,7 +152,7 @@ export type SyncDeltaRow = z.infer<typeof syncDeltaRowSchema>;
152
152
  * hand-coding the boundary; the BYO outbox writes the `tenant` slice and the
153
153
  * relay enriches the `control` slices in Ablo's own database.
154
154
  */
155
- export declare const DELTA_PLANES: {
155
+ export declare const DELTA_RESIDENCY: {
156
156
  readonly core: "tenant";
157
157
  readonly attribution: "control";
158
158
  readonly provenance: "control";
@@ -17,7 +17,7 @@
17
17
  * - {@link deltaProvenanceSchema} — which AI task caused it. `control` plane.
18
18
  *
19
19
  * {@link syncDeltaRowSchema} is the full stored row (core ∪ attribution ∪
20
- * provenance). {@link DELTA_PLANES} declares each slice's plane so provisioning
20
+ * provenance). {@link DELTA_RESIDENCY} declares each slice's plane so provisioning
21
21
  * (P1) can derive "what a customer DB gets" from the schema, not hand-code it.
22
22
  *
23
23
  * Distinct from the WIRE `SyncDelta` (`sync/SyncWebSocket.ts`, client-facing) and
@@ -96,7 +96,7 @@ export const syncDeltaRowSchema = syncDeltaCoreSchema
96
96
  * hand-coding the boundary; the BYO outbox writes the `tenant` slice and the
97
97
  * relay enriches the `control` slices in Ablo's own database.
98
98
  */
99
- export const DELTA_PLANES = {
99
+ export const DELTA_RESIDENCY = {
100
100
  core: 'tenant',
101
101
  attribution: 'control',
102
102
  provenance: 'control',
@@ -1 +1,8 @@
1
- export {};
1
+ /** See module note — host-deployment vocabulary, never customer-facing. */
2
+ export declare const storageModeSchema: z.ZodEnum<{
3
+ source: "source";
4
+ hosted: "hosted";
5
+ selfHosted: "selfHosted";
6
+ }>;
7
+ /** See module note — host-deployment vocabulary, never customer-facing. */
8
+ export type StorageMode = z.infer<typeof storageModeSchema>;
@@ -8,11 +8,21 @@
8
8
  * - `selfHosted` — the customer's database, same execution path as hosted.
9
9
  * - `source` — a customer-owned endpoint (credentialless ingestion).
10
10
  *
11
- * @internal Deployment topology, not product vocabulary. Customers never see a
12
- * "storage mode" — their story is `Ablo({ schema, apiKey, databaseUrl })` and
13
- * one `datasource` resource (docs/plans/sync-engine-stripe-story-scope.md).
11
+ * Deployment topology, not product vocabulary. Customers never see a
12
+ * "storage mode" — their story is `ablo connect` (WAL replication: reads tail
13
+ * their database's logical-replication stream, the data stays in their DB)
14
+ * with the signed `dataSource()` endpoint as the marked fallback (ADR 0002,
15
+ * docs/decisions/0002-read-path-logical-replication-vs-data-hosting.md).
16
+ * The `databaseUrl` dial-in that this enum's `selfHosted` arm serves is
17
+ * DEPRECATED pending the WAL read cutover — do not build on it.
14
18
  * This export exists for the sync-server host only.
19
+ *
20
+ * NOT `@internal`-tagged: tsconfig.build.json sets `stripInternal`, and these
21
+ * symbols are re-exported by `server/index.ts` — tagging them strips them from
22
+ * the emitted d.ts, leaving an empty `export {}` module behind a live
23
+ * re-export. `skipLibCheck` masks the break and `StorageMode` silently
24
+ * degrades to an error-any for every dist consumer (the sync-server host).
15
25
  */
16
26
  import { z } from 'zod';
17
- /** @internal See module note — host-deployment vocabulary, never customer-facing. */
27
+ /** See module note — host-deployment vocabulary, never customer-facing. */
18
28
  export const storageModeSchema = z.enum(['hosted', 'source', 'selfHosted']);
@@ -17,7 +17,7 @@
17
17
  * core handler's `commit` / `events` / per-model `load`+`list` — no per-ORM
18
18
  * branching anywhere above the adapter.
19
19
  */
20
- import type { SourceListQuery, SourceRequestContext } from './index.js';
20
+ import type { SourceListQuery, SourceRequestContext } from './types.js';
21
21
  import type { AdapterCapabilities, ChangeSet, EventsPage, Migration } from './contract.js';
22
22
  /**
23
23
  * A canonical row keyed by schema FIELD name (e.g. `operatorId`) — the SDK shape,
@@ -144,8 +144,9 @@ export function drizzleDataSource(db, schema) {
144
144
  async commit(change) {
145
145
  return db.transaction(async (tx) => {
146
146
  const cached = rowsOf(await tx.execute(sql `SELECT response FROM ablo_idempotency WHERE client_tx_id = ${change.clientTxId} LIMIT 1`));
147
- if (cached.length > 0)
148
- return { rows: cached[0].response };
147
+ const cachedRow = cached[0];
148
+ if (cachedRow)
149
+ return { rows: cachedRow.response };
149
150
  const rows = [];
150
151
  for (const [index, op] of change.operations.entries()) {
151
152
  const row = await applyOperation(tx, op);
@@ -180,7 +181,7 @@ export function drizzleDataSource(db, schema) {
180
181
  occurredAt: r.occurred_at != null ? Number(r.occurred_at) : null,
181
182
  cursor: String(r.cursor),
182
183
  }));
183
- return { events, nextCursor: events.length > 0 ? events[events.length - 1].cursor : null };
184
+ return { events, nextCursor: events.at(-1)?.cursor ?? null };
184
185
  },
185
186
  };
186
187
  }
@@ -153,8 +153,9 @@ export function kyselyDataSource(db, schema) {
153
153
  .where('client_tx_id', '=', change.clientTxId)
154
154
  .limit(1)
155
155
  .execute();
156
- if (cached.length > 0) {
157
- const response = cached[0].response;
156
+ const cachedRow = cached[0];
157
+ if (cachedRow) {
158
+ const response = cachedRow.response;
158
159
  return {
159
160
  rows: (typeof response === 'string' ? JSON.parse(response) : response),
160
161
  };
@@ -204,7 +205,7 @@ export function kyselyDataSource(db, schema) {
204
205
  occurredAt: r.occurred_at != null ? Number(r.occurred_at) : null,
205
206
  cursor: String(r.cursor),
206
207
  }));
207
- return { events, nextCursor: events.length > 0 ? events[events.length - 1].cursor : null };
208
+ return { events, nextCursor: events.at(-1)?.cursor ?? null };
208
209
  },
209
210
  };
210
211
  }
@@ -108,7 +108,7 @@ export function memoryDataSource() {
108
108
  const page = outbox.filter((e) => Number(e.cursor) > after).slice(0, limit);
109
109
  return {
110
110
  events: page,
111
- nextCursor: page.length > 0 ? page[page.length - 1].cursor : null,
111
+ nextCursor: page.at(-1)?.cursor ?? null,
112
112
  };
113
113
  },
114
114
  };
@@ -15,7 +15,7 @@
15
15
  import { AbloValidationError } from '../../errors.js';
16
16
  import { outboxEventSchema } from '../contract.js';
17
17
  import { adapterTableMigrations } from '../migrations.js';
18
- const lowerFirst = (s) => (s ? s[0].toLowerCase() + s.slice(1) : s);
18
+ const lowerFirst = (s) => (s ? s.charAt(0).toLowerCase() + s.slice(1) : s);
19
19
  /**
20
20
  * Resolve a model's Prisma delegate by name. This is the ONE irreducible cast in
21
21
  * the adapter layer, and it's a genuine type-system limit, not laziness:
@@ -138,8 +138,9 @@ export function prismaDataSource(prisma, schema, options = {}) {
138
138
  return prisma.$transaction(async (tx) => {
139
139
  // Idempotency: a duplicate clientTxId returns the original rows, no re-apply.
140
140
  const cached = await tx.$queryRawUnsafe(`SELECT response FROM ablo_idempotency WHERE client_tx_id = $1 LIMIT 1`, change.clientTxId);
141
- if (cached.length > 0)
142
- return { rows: cached[0].response };
141
+ const cachedRow = cached[0];
142
+ if (cachedRow)
143
+ return { rows: cachedRow.response };
143
144
  const rows = [];
144
145
  for (const [index, op] of change.operations.entries()) {
145
146
  const row = await applyOperation(tx, op);
@@ -170,7 +171,7 @@ export function prismaDataSource(prisma, schema, options = {}) {
170
171
  }));
171
172
  return {
172
173
  events,
173
- nextCursor: events.length > 0 ? events[events.length - 1].cursor : null,
174
+ nextCursor: events.at(-1)?.cursor ?? null,
174
175
  };
175
176
  },
176
177
  };
@@ -29,8 +29,10 @@ export function dataSourceConformanceChecks(make) {
29
29
  const adapter = await make();
30
30
  const result = await adapter.commit(change('tx_create', [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } }]));
31
31
  assert.equal(result.rows.length, 1, 'one row returned');
32
- assert.equal(result.rows[0].id, 't1');
33
- assert.equal(result.rows[0].title, 'A');
32
+ const created = result.rows[0];
33
+ assert.ok(created, 'one row returned');
34
+ assert.equal(created.id, 't1');
35
+ assert.equal(created.title, 'A');
34
36
  },
35
37
  },
36
38
  {
@@ -40,7 +42,7 @@ export function dataSourceConformanceChecks(make) {
40
42
  await adapter.commit(change('tx1', [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } }]));
41
43
  const found = await adapter.read({ kind: 'load', model: 'task', id: 't1' });
42
44
  assert.equal(found.length, 1);
43
- assert.equal(found[0].title, 'A');
45
+ assert.equal(found[0]?.title, 'A');
44
46
  const missing = await adapter.read({ kind: 'load', model: 'task', id: 'nope' });
45
47
  assert.equal(missing.length, 0, 'unknown id reads empty');
46
48
  },
@@ -54,7 +56,7 @@ export function dataSourceConformanceChecks(make) {
54
56
  { type: 'CREATE', model: 'task', id: 't2', input: { title: 'B' } },
55
57
  ]));
56
58
  const rows = await adapter.read({ kind: 'list', model: 'task' });
57
- const ids = rows.map((r) => r.id).sort();
59
+ const ids = rows.map((r) => (r).id).sort();
58
60
  assert.deepEqual(ids, ['t1', 't2']);
59
61
  },
60
62
  },
@@ -83,9 +85,9 @@ export function dataSourceConformanceChecks(make) {
83
85
  assert.ok(page.events.length >= 1, 'at least one event');
84
86
  const evt = page.events.find((e) => e.entityId === 't1');
85
87
  assert.ok(evt, 'event for the committed row');
86
- assert.equal(evt?.model, 'task');
87
- assert.equal(evt?.type, 'CREATE');
88
- assert.equal(evt?.clientTxId, 'tx_evt');
88
+ assert.equal(evt.model, 'task');
89
+ assert.equal(evt.type, 'CREATE');
90
+ assert.equal(evt.clientTxId, 'tx_evt');
89
91
  },
90
92
  },
91
93
  {
@@ -115,7 +117,7 @@ export function dataSourceConformanceChecks(make) {
115
117
  await adapter.commit(change('tx_c1', [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } }]));
116
118
  await adapter.commit(change('tx_u1', [{ type: 'UPDATE', model: 'task', id: 't1', input: { title: 'B' } }]));
117
119
  const found = await adapter.read({ kind: 'load', model: 'task', id: 't1' });
118
- assert.equal(found[0].title, 'B', 'update applied');
120
+ assert.equal(found[0]?.title, 'B', 'update applied');
119
121
  },
120
122
  },
121
123
  ];
@@ -26,8 +26,10 @@
26
26
  * path. The transport changes; the trust model does not.
27
27
  */
28
28
  import { SOURCE_CONNECTOR_PROTOCOL_VERSION, SOURCE_CONNECTOR_WS_PATH, sourceConnectorSubprotocols, encodeFrame, decodeFrame, ConnectorProtocolError, } from './connector-protocol.js';
29
- /** Default Ablo Cloud base. The connector appends `SOURCE_CONNECTOR_WS_PATH`. */
30
- const DEFAULT_BASE_URL = 'https://api.abloatai.com';
29
+ import { ABLO_HOSTED_HTTP_BASE_URL } from '../client/hostedEndpoints.js';
30
+ /** Default Ablo Cloud base. The connector appends `SOURCE_CONNECTOR_WS_PATH`.
31
+ * Single-sourced from the dependency-free hosted-endpoints leaf. */
32
+ const DEFAULT_BASE_URL = ABLO_HOSTED_HTTP_BASE_URL;
31
33
  /**
32
34
  * Reconnect backoff, in ms, indexed by consecutive failed connect attempts.
33
35
  * Unlike the (multi-day) Standard Webhooks delivery schedule, a long-lived
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The `abloSource()` / `dataSource()` endpoint factory — the customer-owned
3
+ * Data Source handler. Takes the options (schema, apiKey, handlers or ORM
4
+ * adapter), verifies the signed request, enforces per-key scopes, and
5
+ * dispatches the four wire operations (`load`/`list`/`commit`/`events`) to
6
+ * the configured handlers.
7
+ *
8
+ * `AbloSourceOptions` lives here (not `types.ts`) because it references the
9
+ * ORM `DataSourceAdapter` — keeping it with the factory keeps `types.ts` a
10
+ * dependency-free leaf for `adapter.ts`/`contract.ts` to import from.
11
+ */
12
+ import type { Schema, SchemaRecord, InferCreate } from '../schema/schema.js';
13
+ import type { DataSourceAdapter } from './adapter.js';
14
+ import type { SourceApiKey, SourceAuthorizeContext, SourceCommitHandler, SourceEventsHandler, SourceModelHandlers, SourceRequest, SourceScope } from './types.js';
15
+ type SourceModels<S extends SchemaRecord, TAuth> = Partial<{
16
+ readonly [K in keyof S & string]: SourceModelHandlers<InferCreate<Schema<S>, K> & {
17
+ readonly id: string;
18
+ } & Record<string, unknown>, InferCreate<Schema<S>, K>, TAuth>;
19
+ }>;
20
+ export type AbloSourceOptions<S extends SchemaRecord, TAuth = unknown> = {
21
+ readonly schema: Schema<S>;
22
+ /**
23
+ * Customer-visible Ablo credential. In the API-key-only onboarding
24
+ * path, Ablo signs Data Source calls with the same project API key
25
+ * that the customer's server-side SDK uses. This keeps the customer
26
+ * env surface to one Ablo credential while preserving signed request
27
+ * verification before any handler runs.
28
+ */
29
+ readonly apiKey: SourceApiKey;
30
+ /**
31
+ * Clock-skew window for signed source requests. Default: 5 minutes.
32
+ */
33
+ readonly signatureToleranceMs?: number;
34
+ /**
35
+ * Verify the Ablo request and return customer-owned context such as
36
+ * a database handle, account scope, or current actor. Keep database
37
+ * credentials in this function's environment; never send them to Ablo.
38
+ *
39
+ * Signature verification is handled by `apiKey` before this function
40
+ * runs. `authorize` should only attach business context.
41
+ */
42
+ readonly authorize?: (context: SourceAuthorizeContext) => Promise<TAuth> | TAuth;
43
+ /**
44
+ * Optional per-request scope resolver. When set, the helper checks
45
+ * the resolved scope set against the request's operation
46
+ * (`load`/`list`/`commit`/`events`) and returns 403
47
+ * `source_forbidden` if not allowed — before any model handler
48
+ * runs.
49
+ *
50
+ * Customers typically extract a key id from the request (e.g.
51
+ * `webhook-id` prefix, a custom header, or the API key itself) and
52
+ * look up the scopes for that key in their store.
53
+ *
54
+ * When omitted, all operations are allowed. Returning an empty set
55
+ * denies all operations.
56
+ */
57
+ readonly resolveScopes?: (params: {
58
+ readonly auth: TAuth;
59
+ readonly request: Request;
60
+ readonly body: SourceRequest;
61
+ }) => Promise<ReadonlySet<SourceScope> | readonly SourceScope[]> | ReadonlySet<SourceScope> | readonly SourceScope[];
62
+ /**
63
+ * Top-level atomic commit handler. Prefer this for real applications:
64
+ * one UI/action commit can span several models and should run inside
65
+ * one customer-owned transaction.
66
+ */
67
+ readonly commit?: SourceCommitHandler<TAuth>;
68
+ /**
69
+ * External-write feed. Ablo polls this to learn about changes that
70
+ * happened outside the SDK (cron jobs, dashboard edits, batch
71
+ * imports). Each returned event becomes a delta and fans out to
72
+ * connected clients.
73
+ *
74
+ * Handlers may return the raw outbox feed. Ablo dedupes stable
75
+ * `event.id` values and filters SDK-origin echoes when rows carry
76
+ * the originating `clientTxId`; customers should persist both fields
77
+ * in their outbox table.
78
+ */
79
+ readonly events?: SourceEventsHandler<TAuth>;
80
+ /**
81
+ * Optional grouped form. The object-key form below is usually terser:
82
+ * `abloSource({ schema, files: { load, list, commit } })`.
83
+ */
84
+ readonly models?: SourceModels<S, TAuth>;
85
+ /**
86
+ * An ORM adapter (`prismaDataSource(prisma, schema)`, …). When set, it serves
87
+ * ALL four operations — read (load/list), commit (idempotent + outbox), and
88
+ * events — so no hand-written `commit`/`events`/model handlers are needed. The
89
+ * adapter is consumed at the generic dispatch layer (rows are JSON on the wire),
90
+ * which is why it carries no per-model types and needs no cast at the call site.
91
+ * Mutually exclusive with hand-written handlers.
92
+ */
93
+ readonly adapter?: DataSourceAdapter;
94
+ } & SourceModels<S, TAuth>;
95
+ /**
96
+ * Create a customer-owned data source endpoint.
97
+ *
98
+ * App code still talks to Ablo with `ablo.files.load/list/update`.
99
+ * This helper is only for customers who keep canonical rows in their own
100
+ * database and want Ablo Cloud to call a narrow, signed endpoint instead
101
+ * of receiving database credentials.
102
+ */
103
+ export declare function abloSource<const S extends SchemaRecord, TAuth = unknown>(options: AbloSourceOptions<S, TAuth>): (request: Request) => Promise<Response>;
104
+ export type DataSourceOptions<S extends SchemaRecord, TAuth = unknown> = AbloSourceOptions<S, TAuth>;
105
+ export declare function dataSource<const S extends SchemaRecord, TAuth = unknown>(options: DataSourceOptions<S, TAuth>): (request: Request) => Promise<Response>;
106
+ export {};