@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
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Standard Webhooks request signing/verification for Data Source calls.
3
+ *
4
+ * Ablo signs every outbound source request with the customer's project API
5
+ * key; the customer's endpoint verifies before any handler runs. Both halves
6
+ * live in this leaf so `pushQueue.ts` (customer-side sender) and `factory.ts`
7
+ * (customer-side receiver) can import them directly without a runtime cycle
8
+ * through the `index.ts` barrel.
9
+ */
10
+ /**
11
+ * HTTP headers used on signed source requests. Conforms to the
12
+ * Standard Webhooks specification (https://www.standardwebhooks.com/)
13
+ * so customer code can verify our signatures with any of the official
14
+ * libraries (svix, standardwebhooks, hookdeck, etc.) — no Ablo-
15
+ * specific verifier required.
16
+ */
17
+ export const ABLO_SOURCE_HEADERS = {
18
+ signature: 'webhook-signature',
19
+ timestamp: 'webhook-timestamp',
20
+ id: 'webhook-id',
21
+ idempotencyKey: 'Idempotency-Key',
22
+ };
23
+ export class SourceSignatureError extends Error {
24
+ code;
25
+ constructor(code, message) {
26
+ super(message);
27
+ this.name = 'SourceSignatureError';
28
+ this.code = code;
29
+ }
30
+ }
31
+ const DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
32
+ function getHeader(request, name) {
33
+ const headers = request.headers;
34
+ if (!headers)
35
+ return null;
36
+ if (typeof headers.get === 'function') {
37
+ return headers.get(name);
38
+ }
39
+ const record = headers;
40
+ return record[name] ?? record[name.toLowerCase()] ?? null;
41
+ }
42
+ /**
43
+ * Parse a `webhook-signature` header per the Standard Webhooks spec.
44
+ * Values are space-delimited `<scheme>,<base64>` pairs (e.g.
45
+ * `v1,abc== v1,def==` during a key rotation window). Returns the set
46
+ * of `v1` signatures so the verifier can accept any of them.
47
+ */
48
+ function parseSignatureHeader(raw) {
49
+ if (!raw)
50
+ return [];
51
+ const out = [];
52
+ for (const part of raw.split(/\s+/)) {
53
+ const trimmed = part.trim();
54
+ if (!trimmed)
55
+ continue;
56
+ const commaAt = trimmed.indexOf(',');
57
+ if (commaAt === -1)
58
+ continue;
59
+ const scheme = trimmed.slice(0, commaAt);
60
+ const value = trimmed.slice(commaAt + 1);
61
+ if (scheme === 'v1' && value.length > 0) {
62
+ out.push(value);
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+ function bufferToBase64(buffer) {
68
+ // Node + browsers both expose `btoa` on the global; we feed it
69
+ // a binary string built from the byte view.
70
+ let binary = '';
71
+ for (const byte of new Uint8Array(buffer)) {
72
+ binary += String.fromCharCode(byte);
73
+ }
74
+ return btoa(binary);
75
+ }
76
+ async function hmacSha256Base64(apiKey, payload) {
77
+ const crypto = globalThis.crypto?.subtle;
78
+ if (!crypto) {
79
+ throw new SourceSignatureError('source_signature_invalid', 'WebCrypto HMAC support is unavailable in this runtime');
80
+ }
81
+ const encoder = new TextEncoder();
82
+ const key = await crypto.importKey('raw', encoder.encode(apiKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
83
+ return bufferToBase64(await crypto.sign('HMAC', key, encoder.encode(payload)));
84
+ }
85
+ /**
86
+ * Constant-time string equality. Used over `===` so a malicious
87
+ * signature can't be probed byte-by-byte via timing differences.
88
+ */
89
+ function timingSafeEqual(expected, actual) {
90
+ const max = Math.max(expected.length, actual.length);
91
+ let diff = expected.length ^ actual.length;
92
+ for (let i = 0; i < max; i++) {
93
+ diff |= (expected.charCodeAt(i) || 0) ^ (actual.charCodeAt(i) || 0);
94
+ }
95
+ return diff === 0;
96
+ }
97
+ export async function signAbloSourceRequest(options) {
98
+ const signedAt = options.timestamp ?? Math.floor(Date.now() / 1000);
99
+ // Standard Webhooks signing input: `${msg_id}.${timestamp}.${payload}`
100
+ const signature = await hmacSha256Base64(options.apiKey, `${options.messageId}.${signedAt}.${options.body}`);
101
+ return {
102
+ signedAt,
103
+ signature,
104
+ headers: {
105
+ [ABLO_SOURCE_HEADERS.id]: options.messageId,
106
+ [ABLO_SOURCE_HEADERS.timestamp]: String(signedAt),
107
+ [ABLO_SOURCE_HEADERS.signature]: `v1,${signature}`,
108
+ },
109
+ };
110
+ }
111
+ export async function verifyAbloSourceRequest(options) {
112
+ const messageId = getHeader(options.request, ABLO_SOURCE_HEADERS.id);
113
+ if (!messageId) {
114
+ throw new SourceSignatureError('source_id_missing', 'Missing webhook-id header');
115
+ }
116
+ const rawTimestamp = getHeader(options.request, ABLO_SOURCE_HEADERS.timestamp);
117
+ if (!rawTimestamp) {
118
+ throw new SourceSignatureError('source_timestamp_missing', 'Missing webhook-timestamp header');
119
+ }
120
+ const signedAt = Number(rawTimestamp);
121
+ if (!Number.isFinite(signedAt)) {
122
+ throw new SourceSignatureError('source_timestamp_invalid', 'Invalid webhook-timestamp header');
123
+ }
124
+ const toleranceMs = options.toleranceMs ?? DEFAULT_SIGNATURE_TOLERANCE_MS;
125
+ const nowSeconds = Math.floor(Date.now() / 1000);
126
+ const toleranceSeconds = Math.ceil(toleranceMs / 1000);
127
+ if (Math.abs(nowSeconds - signedAt) > toleranceSeconds) {
128
+ throw new SourceSignatureError('source_timestamp_expired', 'webhook-timestamp is outside the allowed clock-skew window');
129
+ }
130
+ const presented = parseSignatureHeader(getHeader(options.request, ABLO_SOURCE_HEADERS.signature));
131
+ if (presented.length === 0) {
132
+ throw new SourceSignatureError('source_signature_missing', 'Missing webhook-signature header');
133
+ }
134
+ const expected = await hmacSha256Base64(options.apiKey, `${messageId}.${signedAt}.${options.body}`);
135
+ // Accept any presented signature that matches — supports key
136
+ // rotation per the Standard Webhooks spec.
137
+ const ok = presented.some((sig) => timingSafeEqual(expected, sig));
138
+ if (!ok) {
139
+ throw new SourceSignatureError('source_signature_invalid', 'Invalid webhook-signature');
140
+ }
141
+ return { messageId, signedAt };
142
+ }
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Shared Data Source wire + handler types.
3
+ *
4
+ * These are the cross-package shapes every source module speaks (operations,
5
+ * events, list queries, handler contexts, the four wire request types). They
6
+ * live in this leaf — not the `index.ts` barrel — so `contract.ts`,
7
+ * `adapter.ts`, `pushQueue.ts` and the ORM adapters can import them directly
8
+ * without routing a circular dependency through the barrel.
9
+ *
10
+ * `sourceEventForOperation` lives here too: it is the pure constructor for the
11
+ * `SourceEvent` marker shape and has no dependency on the endpoint factory.
12
+ */
13
+ import type { Environment } from '../environment.js';
14
+ export type SourcePrimitive = string | number | boolean | null;
15
+ export type SourceWhere = readonly [field: string, value: SourcePrimitive] | readonly [
16
+ field: string,
17
+ op: '=' | '!=' | '<' | '<=' | '>' | '>=' | 'IN' | 'NOT IN' | 'IS' | 'IS NOT' | 'LIKE' | 'NOT LIKE' | 'ILIKE' | 'NOT ILIKE',
18
+ value: SourcePrimitive | readonly SourcePrimitive[]
19
+ ];
20
+ export interface SourceListQuery {
21
+ readonly where?: readonly SourceWhere[];
22
+ readonly limit?: number;
23
+ readonly orderBy?: string;
24
+ readonly order?: 'asc' | 'desc';
25
+ readonly related?: readonly string[];
26
+ /**
27
+ * Opaque cursor returned by a previous `list` call. The customer's
28
+ * `list` handler defines what this encodes (page index, last id,
29
+ * keyset). Ablo treats it as a black box — round-trips it back to
30
+ * fetch the next page until the handler returns no `nextCursor`.
31
+ */
32
+ readonly cursor?: string;
33
+ }
34
+ /**
35
+ * Optional structured shape for a `list` handler that supports
36
+ * pagination. Handlers may keep returning a plain `Row[]` (no
37
+ * pagination, single-shot) or upgrade to this shape to expose a
38
+ * cursor that Ablo will round-trip on the next request.
39
+ */
40
+ export interface SourceListPage<Row> {
41
+ readonly rows: readonly Row[];
42
+ readonly nextCursor?: string;
43
+ }
44
+ export type SourceListResult<Row> = readonly Row[] | SourceListPage<Row>;
45
+ /**
46
+ * Read-side scope context that Ablo attaches to source requests so
47
+ * the customer's `authorize` / model handlers can refuse calls that
48
+ * fall outside the participant's permitted syncGroups.
49
+ *
50
+ * This is informational — the customer is the only side that can
51
+ * actually enforce, since the canonical data lives in their store.
52
+ * Mirrors how Auth0 Custom DB scripts receive the requested scope and
53
+ * trust the script to honor it.
54
+ */
55
+ export interface SourceRequestContext {
56
+ readonly participantId?: string;
57
+ readonly participantKind?: 'user' | 'agent' | 'system';
58
+ readonly organizationId?: string;
59
+ readonly requiredSyncGroups?: readonly string[];
60
+ /**
61
+ * Production/sandbox mode for this request. Customers branch their source
62
+ * handlers on this (`if (mode === 'sandbox') db = sandboxDb`) so sandbox
63
+ * traffic exercises the same code path against an isolated store.
64
+ *
65
+ * Mirrors Stripe's `sk_test_` / `sk_live_` prefixes: same wire
66
+ * shape, same handler code, different namespace. Ablo's server-side
67
+ * fan-out does not yet partition deltas by mode — that lands when
68
+ * `sync_deltas.mode` ships. Until then, isolation is enforced
69
+ * customer-side via this field, which is the right boundary anyway
70
+ * (the customer's database is where the canonical data lives).
71
+ *
72
+ * Defaults to `'production'` when omitted so callers that don't opt in
73
+ * keep the existing behavior.
74
+ */
75
+ readonly mode?: Environment;
76
+ }
77
+ export interface SourceOperation {
78
+ readonly type: 'CREATE' | 'UPDATE' | 'DELETE' | 'ARCHIVE' | 'UNARCHIVE';
79
+ readonly model: string;
80
+ readonly id?: string | null;
81
+ readonly input?: Record<string, unknown> | null;
82
+ readonly transactionId?: string | null;
83
+ readonly readAt?: number | null;
84
+ readonly onStale?: 'reject' | 'overwrite' | 'notify' | null;
85
+ }
86
+ export interface SourceDelta {
87
+ readonly model: string;
88
+ readonly id: string;
89
+ readonly type: SourceOperation['type'];
90
+ readonly data?: Record<string, unknown> | null;
91
+ readonly transactionId?: string | null;
92
+ }
93
+ /**
94
+ * A change that happened in the customer's store. The source's
95
+ * `events` handler returns these so Ablo can append them to
96
+ * `sync_deltas` and fan them out to connected clients exactly like
97
+ * SDK-originated commits.
98
+ *
99
+ * The events handler can return everything from the outbox unfiltered. Ablo
100
+ * dedupes stable `event.id` values and uses `clientTxId` to filter SDK-origin
101
+ * echoes after the direct append has already succeeded. If the direct append
102
+ * failed, the same outbox event repairs it on poll/push because no matching
103
+ * `mutation_log` row exists yet.
104
+ */
105
+ export interface SourceEvent {
106
+ /**
107
+ * Globally unique event id from the customer's outbox. Used by Ablo
108
+ * for replay protection — re-delivering the same id is a no-op.
109
+ */
110
+ readonly id: string;
111
+ readonly model: string;
112
+ readonly entityId: string;
113
+ readonly type: SourceOperation['type'];
114
+ readonly data?: Record<string, unknown> | null;
115
+ /**
116
+ * Tenant the event belongs to. Multi-tenant customers populate this
117
+ * from the row's organization column. Single-tenant deployments may
118
+ * omit it and let the poller fall back to its configured default.
119
+ * Drives the sync-group fan-out: clients in `org:${organizationId}`
120
+ * receive the resulting delta.
121
+ */
122
+ readonly organizationId?: string;
123
+ /**
124
+ * Originating Ablo SDK commit id, when known. If the customer's
125
+ * outbox stores the `clientTxId` Ablo passed into the matching
126
+ * `commit` handler, round-trip it here and Ablo will skip events
127
+ * whose commit already produced a delta. External-origin events
128
+ * (cron jobs, batch imports, manual edits) leave this unset.
129
+ */
130
+ readonly clientTxId?: string;
131
+ /**
132
+ * Wall-clock time the event occurred in the source. Optional; used
133
+ * only for ordering hints. Ablo trusts the customer's response order
134
+ * over this field.
135
+ */
136
+ readonly occurredAt?: number;
137
+ }
138
+ export interface SourceEventForOperationOptions {
139
+ /**
140
+ * Stable id from the customer's outbox table. This is Ablo's replay-
141
+ * protection key; retries must return the same id.
142
+ */
143
+ readonly eventId: string;
144
+ readonly operation: SourceOperation;
145
+ /**
146
+ * Committed row id. Defaults to `operation.id`; pass this for generated-id
147
+ * CREATEs where the database assigns the id inside the transaction.
148
+ */
149
+ readonly entityId?: string;
150
+ /**
151
+ * Canonical row payload after the write. Pass `null` for DELETE. When omitted
152
+ * the event carries no row payload, which is valid but less useful for
153
+ * realtime hydration.
154
+ */
155
+ readonly data?: Record<string, unknown> | null;
156
+ /**
157
+ * Batch idempotency key from the Data Source commit request. Round-tripping it
158
+ * lets Ablo filter SDK-origin echoes after the direct append succeeds, while
159
+ * still using the outbox event to repair a failed direct append.
160
+ */
161
+ readonly clientTxId?: string;
162
+ readonly organizationId?: string;
163
+ readonly occurredAt?: number | Date;
164
+ }
165
+ /**
166
+ * Build the source-event marker customers should write to their outbox table in
167
+ * the SAME transaction as their app-row mutation.
168
+ *
169
+ * This helper does not persist anything. It only standardizes the marker shape
170
+ * so Prisma/Drizzle/Kysely/raw-SQL adapters all emit the fields Ablo's
171
+ * reconciler expects.
172
+ */
173
+ export declare function sourceEventForOperation(options: SourceEventForOperationOptions): SourceEvent;
174
+ export interface SourceCommitResult<Row = Record<string, unknown>> {
175
+ /**
176
+ * Canonical rows after the write. Ablo uses these to update hosted
177
+ * realtime projections and append deltas.
178
+ */
179
+ readonly rows?: readonly Row[];
180
+ /**
181
+ * Optional explicit deltas when the source already computes them.
182
+ * Most sources can return rows and let Ablo derive the delta payload.
183
+ */
184
+ readonly deltas?: readonly SourceDelta[];
185
+ }
186
+ export interface SourceCommitParams<TAuth = unknown> {
187
+ readonly operations: readonly SourceOperation[];
188
+ readonly clientTxId?: string;
189
+ readonly context: SourceHandlerContext<TAuth>;
190
+ }
191
+ /**
192
+ * Operation-level permission tag used by `resolveScopes`. Mirrors the
193
+ * four wire request types: an API key carries the set of operations
194
+ * it's allowed to invoke. Stripe's restricted-key model at the
195
+ * operation granularity — model-level scoping is a future addition.
196
+ */
197
+ export type SourceScope = 'load' | 'list' | 'commit' | 'events';
198
+ export interface SourceEventsResult {
199
+ readonly events: readonly SourceEvent[];
200
+ /**
201
+ * Cursor for the next poll. When omitted Ablo treats the feed as
202
+ * fully drained for this round and uses the last event's cursor (or
203
+ * the initial cursor) for the next call.
204
+ */
205
+ readonly nextCursor?: string;
206
+ }
207
+ export type SourceEventsHandler<TAuth = unknown> = (params: {
208
+ /**
209
+ * Cursor returned by a previous `events` call. Undefined on the
210
+ * first poll for a freshly-onboarded source. The customer decides
211
+ * what it encodes (last event id, timestamp, LSN, etc).
212
+ */
213
+ readonly cursor?: string;
214
+ /**
215
+ * Caller-suggested upper bound on returned events. Customers may
216
+ * return fewer; returning more risks tripping Ablo's per-poll cap.
217
+ */
218
+ readonly limit?: number;
219
+ readonly context: SourceHandlerContext<TAuth>;
220
+ }) => Promise<SourceEventsResult> | SourceEventsResult;
221
+ export interface SourceAuthorizeContext {
222
+ readonly request: Request;
223
+ readonly body: unknown;
224
+ readonly rawBody: string;
225
+ }
226
+ export interface SourceHandlerContext<TAuth = unknown> {
227
+ readonly auth: TAuth;
228
+ readonly request: Request;
229
+ /**
230
+ * `webhook-id` from the signed request — globally unique per the
231
+ * Standard Webhooks spec. Customers should dedupe by this id to
232
+ * defend against replay (Ablo doesn't dedupe at the source-handler
233
+ * boundary; commit idempotency is `clientTxId`, and event replay
234
+ * protection is the outbox event `id`).
235
+ */
236
+ readonly messageId?: string;
237
+ readonly signedAt?: number;
238
+ /**
239
+ * Scope context Ablo attached to this request. Present when the
240
+ * caller (sync-server) opted into scope-aware source mode. Customers
241
+ * can use it in `authorize` (to reject out-of-scope calls) and in
242
+ * `list` / `load` (to filter rows the participant is allowed to see).
243
+ *
244
+ * Absent for calls made without scope context, such as tests or
245
+ * single-tenant deployments that do not need scoped fan-out yet.
246
+ */
247
+ readonly scope?: SourceRequestContext;
248
+ }
249
+ export interface SourceModelHandlers<Row, CreateInput, TAuth = unknown> {
250
+ load?(params: {
251
+ readonly id: string;
252
+ readonly context: SourceHandlerContext<TAuth>;
253
+ }): Promise<Row | null> | Row | null;
254
+ list?(params: {
255
+ readonly query: SourceListQuery;
256
+ readonly context: SourceHandlerContext<TAuth>;
257
+ }): Promise<SourceListResult<Row>> | SourceListResult<Row>;
258
+ /**
259
+ * Apply one or more operations for this model in the customer's own
260
+ * transaction. The source must be idempotent by operation/clientTxId.
261
+ */
262
+ commit?(params: {
263
+ readonly operations: readonly SourceOperation[];
264
+ readonly clientTxId?: string;
265
+ readonly context: SourceHandlerContext<TAuth>;
266
+ }): Promise<SourceCommitResult<Row>> | SourceCommitResult<Row>;
267
+ }
268
+ export type SourceCommitHandler<TAuth = unknown> = (params: SourceCommitParams<TAuth>) => Promise<SourceCommitResult> | SourceCommitResult;
269
+ export type SourceApiKey = string | ((context: SourceAuthorizeContext) => Promise<string> | string);
270
+ export interface SourceLoadRequest {
271
+ readonly type: 'load';
272
+ readonly model: string;
273
+ readonly id: string;
274
+ readonly scope?: SourceRequestContext;
275
+ }
276
+ export interface SourceListRequest {
277
+ readonly type: 'list';
278
+ readonly model: string;
279
+ readonly query?: SourceListQuery;
280
+ readonly scope?: SourceRequestContext;
281
+ }
282
+ export interface SourceCommitRequest {
283
+ readonly type: 'commit';
284
+ /**
285
+ * Optional single-model hint. Omit for cross-model commits; top-level
286
+ * `commit` receives the whole operation array unchanged.
287
+ */
288
+ readonly model?: string;
289
+ readonly operations: readonly SourceOperation[];
290
+ readonly clientTxId?: string;
291
+ readonly scope?: SourceRequestContext;
292
+ }
293
+ export interface SourceEventsRequest {
294
+ readonly type: 'events';
295
+ readonly cursor?: string;
296
+ readonly limit?: number;
297
+ readonly scope?: SourceRequestContext;
298
+ }
299
+ export type SourceRequest = SourceLoadRequest | SourceListRequest | SourceCommitRequest | SourceEventsRequest;
300
+ export type SourceResponse<Row = Record<string, unknown>> = {
301
+ readonly row: Row | null;
302
+ } | {
303
+ readonly rows: readonly Row[];
304
+ readonly nextCursor?: string;
305
+ } | {
306
+ readonly rows?: readonly Row[];
307
+ readonly deltas?: readonly SourceDelta[];
308
+ };
309
+ export type DataSourcePrimitive = SourcePrimitive;
310
+ export type DataSourceWhere = SourceWhere;
311
+ export type DataSourceListQuery = SourceListQuery;
312
+ export type DataSourceListPage<Row> = SourceListPage<Row>;
313
+ export type DataSourceListResult<Row> = SourceListResult<Row>;
314
+ export type DataSourceRequestContext = SourceRequestContext;
315
+ export type DataSourceOperation = SourceOperation;
316
+ export type DataSourceDelta = SourceDelta;
317
+ export type DataSourceEvent = SourceEvent;
318
+ export type DataSourceEventForOperationOptions = SourceEventForOperationOptions;
319
+ export type DataSourceCommitResult<Row = Record<string, unknown>> = SourceCommitResult<Row>;
320
+ export type DataSourceCommitParams<TAuth = unknown> = SourceCommitParams<TAuth>;
321
+ export type DataSourceScope = SourceScope;
322
+ export type DataSourceEventsResult = SourceEventsResult;
323
+ export type DataSourceEventsHandler<TAuth = unknown> = SourceEventsHandler<TAuth>;
324
+ export type DataSourceAuthorizeContext = SourceAuthorizeContext;
325
+ export type DataSourceHandlerContext<TAuth = unknown> = SourceHandlerContext<TAuth>;
326
+ export type DataSourceModelHandlers<Row, CreateInput, TAuth = unknown> = SourceModelHandlers<Row, CreateInput, TAuth>;
327
+ export type DataSourceCommitHandler<TAuth = unknown> = SourceCommitHandler<TAuth>;
328
+ export type DataSourceApiKey = SourceApiKey;
329
+ export type DataSourceLoadRequest = SourceLoadRequest;
330
+ export type DataSourceListRequest = SourceListRequest;
331
+ export type DataSourceCommitRequest = SourceCommitRequest;
332
+ export type DataSourceEventsRequest = SourceEventsRequest;
333
+ export type DataSourceRequest = SourceRequest;
334
+ export type DataSourceResponse<Row = Record<string, unknown>> = SourceResponse<Row>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared Data Source wire + handler types.
3
+ *
4
+ * These are the cross-package shapes every source module speaks (operations,
5
+ * events, list queries, handler contexts, the four wire request types). They
6
+ * live in this leaf — not the `index.ts` barrel — so `contract.ts`,
7
+ * `adapter.ts`, `pushQueue.ts` and the ORM adapters can import them directly
8
+ * without routing a circular dependency through the barrel.
9
+ *
10
+ * `sourceEventForOperation` lives here too: it is the pure constructor for the
11
+ * `SourceEvent` marker shape and has no dependency on the endpoint factory.
12
+ */
13
+ import { AbloValidationError } from '../errors.js';
14
+ /**
15
+ * Build the source-event marker customers should write to their outbox table in
16
+ * the SAME transaction as their app-row mutation.
17
+ *
18
+ * This helper does not persist anything. It only standardizes the marker shape
19
+ * so Prisma/Drizzle/Kysely/raw-SQL adapters all emit the fields Ablo's
20
+ * reconciler expects.
21
+ */
22
+ export function sourceEventForOperation(options) {
23
+ const entityId = options.entityId ?? options.operation.id;
24
+ if (typeof entityId !== 'string' || entityId.length === 0) {
25
+ throw new AbloValidationError('sourceEventForOperation requires operation.id or an explicit entityId', { code: 'source_event_invalid' });
26
+ }
27
+ const occurredAt = normalizeEventOccurredAt(options.occurredAt);
28
+ return {
29
+ id: options.eventId,
30
+ model: options.operation.model,
31
+ entityId,
32
+ type: options.operation.type,
33
+ ...(options.data !== undefined ? { data: options.data } : {}),
34
+ ...(options.organizationId ? { organizationId: options.organizationId } : {}),
35
+ ...(options.clientTxId ? { clientTxId: options.clientTxId } : {}),
36
+ ...(occurredAt !== undefined ? { occurredAt } : {}),
37
+ };
38
+ }
39
+ function normalizeEventOccurredAt(value) {
40
+ if (value === undefined)
41
+ return undefined;
42
+ const timestamp = value instanceof Date ? value.getTime() : value;
43
+ return Number.isFinite(timestamp) ? timestamp : undefined;
44
+ }
@@ -5,7 +5,7 @@
5
5
  * Provides the interface for storing and retrieving models from IndexedDB.
6
6
  * Uses native IndexedDB for maximum performance (no wrapper overhead).
7
7
  */
8
- import { ModelMetadata } from '../types/index.js';
8
+ import type { ModelMetadata } from '../types/index.js';
9
9
  import type { ObjectStoreContract } from './ObjectStoreContract.js';
10
10
  /**
11
11
  * ObjectStore - IDB-backed model storage.
@@ -70,9 +70,9 @@ export class ObjectStore {
70
70
  });
71
71
  const store = tx.objectStore(this.storeName);
72
72
  const request = store.put(data);
73
- tx.oncomplete = () => resolve();
74
- tx.onerror = () => reject(tx.error || new Error('IndexedDB transaction error'));
75
- request.onerror = () => reject(request.error || new Error('IndexedDB request error'));
73
+ tx.oncomplete = () => { resolve(); };
74
+ tx.onerror = () => { reject(tx.error || new Error('IndexedDB transaction error')); };
75
+ request.onerror = () => { reject(request.error || new Error('IndexedDB request error')); };
76
76
  }
77
77
  catch (error) {
78
78
  // Propagate failure so callers do not continue with inconsistent state
@@ -92,8 +92,8 @@ export class ObjectStore {
92
92
  const tx = this.db.transaction([this.storeName], 'readonly');
93
93
  const store = tx.objectStore(this.storeName);
94
94
  const request = store.get(id);
95
- request.onsuccess = () => resolve(request.result);
96
- request.onerror = () => reject(request.error);
95
+ request.onsuccess = () => { resolve(request.result); };
96
+ request.onerror = () => { reject(request.error); };
97
97
  }
98
98
  catch (error) {
99
99
  resolve(undefined);
@@ -132,7 +132,7 @@ export class ObjectStore {
132
132
  }
133
133
  };
134
134
  }
135
- tx.onerror = () => reject(tx.error);
135
+ tx.onerror = () => { reject(tx.error); };
136
136
  }
137
137
  catch (error) {
138
138
  resolve(results);
@@ -151,8 +151,8 @@ export class ObjectStore {
151
151
  const tx = this.db.transaction([this.storeName], 'readonly');
152
152
  const store = tx.objectStore(this.storeName);
153
153
  const request = store.getAll();
154
- request.onsuccess = () => resolve(request.result || []);
155
- request.onerror = () => reject(request.error);
154
+ request.onsuccess = () => { resolve(request.result || []); };
155
+ request.onerror = () => { reject(request.error); };
156
156
  }
157
157
  catch (error) {
158
158
  resolve([]);
@@ -174,9 +174,9 @@ export class ObjectStore {
174
174
  });
175
175
  const store = tx.objectStore(this.storeName);
176
176
  const request = store.delete(id);
177
- tx.oncomplete = () => resolve();
178
- tx.onerror = () => reject(tx.error || new Error('IndexedDB transaction error'));
179
- request.onerror = () => reject(request.error || new Error('IndexedDB request error'));
177
+ tx.oncomplete = () => { resolve(); };
178
+ tx.onerror = () => { reject(tx.error || new Error('IndexedDB transaction error')); };
179
+ request.onerror = () => { reject(request.error || new Error('IndexedDB request error')); };
180
180
  }
181
181
  catch (error) {
182
182
  reject(error instanceof Error ? error : new Error(String(error)));
@@ -210,9 +210,9 @@ export class ObjectStore {
210
210
  });
211
211
  const store = tx.objectStore(this.storeName);
212
212
  const request = store.clear();
213
- tx.oncomplete = () => resolve();
214
- tx.onerror = () => reject(tx.error || new Error('IndexedDB transaction error'));
215
- request.onerror = () => reject(request.error || new Error('IndexedDB request error'));
213
+ tx.oncomplete = () => { resolve(); };
214
+ tx.onerror = () => { reject(tx.error || new Error('IndexedDB transaction error')); };
215
+ request.onerror = () => { reject(request.error || new Error('IndexedDB request error')); };
216
216
  }
217
217
  catch (error) {
218
218
  reject(error instanceof Error ? error : new Error(String(error)));
@@ -231,8 +231,8 @@ export class ObjectStore {
231
231
  const tx = this.db.transaction([this.storeName], 'readonly');
232
232
  const store = tx.objectStore(this.storeName);
233
233
  const request = store.count();
234
- request.onsuccess = () => resolve(request.result);
235
- request.onerror = () => reject(request.error);
234
+ request.onsuccess = () => { resolve(request.result); };
235
+ request.onerror = () => { reject(request.error); };
236
236
  }
237
237
  catch (error) {
238
238
  resolve(0);
@@ -252,8 +252,8 @@ export class ObjectStore {
252
252
  const store = tx.objectStore(this.storeName);
253
253
  const index = store.index(indexName);
254
254
  const request = index.getAll(value);
255
- request.onsuccess = () => resolve(request.result);
256
- request.onerror = () => reject(request.error);
255
+ request.onsuccess = () => { resolve(request.result); };
256
+ request.onerror = () => { reject(request.error); };
257
257
  }
258
258
  catch (error) {
259
259
  resolve([]);
@@ -292,7 +292,7 @@ export class ObjectStore {
292
292
  });
293
293
  resolve(filtered);
294
294
  };
295
- request.onerror = () => reject(request.error);
295
+ request.onerror = () => { reject(request.error); };
296
296
  }
297
297
  catch (error) {
298
298
  resolve([]);
@@ -312,8 +312,8 @@ export class ObjectStore {
312
312
  const store = tx.objectStore(this.storeName);
313
313
  const index = store.index(indexName);
314
314
  const request = index.get(value);
315
- request.onsuccess = () => resolve(request.result);
316
- request.onerror = () => reject(request.error);
315
+ request.onsuccess = () => { resolve(request.result); };
316
+ request.onerror = () => { reject(request.error); };
317
317
  }
318
318
  catch (error) {
319
319
  resolve(undefined);
@@ -333,8 +333,8 @@ export class ObjectStore {
333
333
  const store = tx.objectStore(this.storeName);
334
334
  const index = store.index(indexName);
335
335
  const request = index.count(value);
336
- request.onsuccess = () => resolve(request.result);
337
- request.onerror = () => reject(request.error);
336
+ request.onsuccess = () => { resolve(request.result); };
337
+ request.onerror = () => { reject(request.error); };
338
338
  }
339
339
  catch (error) {
340
340
  resolve(0);
@@ -4,7 +4,7 @@
4
4
  * Stores and manages sync actions received from the server.
5
5
  * Critical for delta sync and maintaining sync state consistency.
6
6
  */
7
- import { SyncAction } from '../types/index.js';
7
+ import type { SyncAction } from '../types/index.js';
8
8
  /**
9
9
  * SyncActionStore - Manages sync actions (deltas)
10
10
  *