@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
@@ -1,422 +1,18 @@
1
- import { AbloValidationError } from '../errors.js';
2
- import { changeSetSchema } from './contract.js';
3
1
  /**
4
- * Build the source-event marker customers should write to their outbox table in
5
- * the SAME transaction as their app-row mutation.
2
+ * `@abloatai/ablo/source` the Data Source barrel.
6
3
  *
7
- * This helper does not persist anything. It only standardizes the marker shape
8
- * so Prisma/Drizzle/Kysely/raw-SQL adapters all emit the fields Ablo's
9
- * reconciler expects.
10
- */
11
- export function sourceEventForOperation(options) {
12
- const entityId = options.entityId ?? options.operation.id;
13
- if (typeof entityId !== 'string' || entityId.length === 0) {
14
- throw new AbloValidationError('sourceEventForOperation requires operation.id or an explicit entityId', { code: 'source_event_invalid' });
15
- }
16
- const occurredAt = normalizeEventOccurredAt(options.occurredAt);
17
- return {
18
- id: options.eventId,
19
- model: options.operation.model,
20
- entityId,
21
- type: options.operation.type,
22
- ...(options.data !== undefined ? { data: options.data } : {}),
23
- ...(options.organizationId ? { organizationId: options.organizationId } : {}),
24
- ...(options.clientTxId ? { clientTxId: options.clientTxId } : {}),
25
- ...(occurredAt !== undefined ? { occurredAt } : {}),
26
- };
27
- }
28
- function normalizeEventOccurredAt(value) {
29
- if (value === undefined)
30
- return undefined;
31
- const timestamp = value instanceof Date ? value.getTime() : value;
32
- return Number.isFinite(timestamp) ? timestamp : undefined;
33
- }
34
- /**
35
- * HTTP headers used on signed source requests. Conforms to the
36
- * Standard Webhooks specification (https://www.standardwebhooks.com/)
37
- * so customer code can verify our signatures with any of the official
38
- * libraries (svix, standardwebhooks, hookdeck, etc.) — no Ablo-
39
- * specific verifier required.
40
- */
41
- export const ABLO_SOURCE_HEADERS = {
42
- signature: 'webhook-signature',
43
- timestamp: 'webhook-timestamp',
44
- id: 'webhook-id',
45
- idempotencyKey: 'Idempotency-Key',
46
- };
47
- export class SourceSignatureError extends Error {
48
- code;
49
- constructor(code, message) {
50
- super(message);
51
- this.name = 'SourceSignatureError';
52
- this.code = code;
53
- }
54
- }
55
- const DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
56
- function json(data, status = 200) {
57
- return new Response(JSON.stringify(data), {
58
- status,
59
- headers: { 'Content-Type': 'application/json' },
60
- });
61
- }
62
- /**
63
- * Serve a request from an ORM `adapter`. Routes the four operations to the adapter
64
- * interface (`read`/`commit`/`events`) and shapes the wire response. The adapter is the
65
- * single point of dispatch — no per-model branching here.
66
- */
67
- async function handleViaAdapter(adapter, body, scope) {
68
- if (body.type === 'load') {
69
- const rows = await adapter.read({
70
- kind: 'load',
71
- model: body.model,
72
- id: body.id,
73
- ...(scope ? { scope } : {}),
74
- });
75
- return json({ row: rows[0] ?? null });
76
- }
77
- if (body.type === 'list') {
78
- const rows = await adapter.read({
79
- kind: 'list',
80
- model: body.model,
81
- ...(body.query ? { query: body.query } : {}),
82
- ...(scope ? { scope } : {}),
83
- });
84
- return json({ rows });
85
- }
86
- if (body.type === 'commit') {
87
- if (!body.clientTxId) {
88
- return json({ error: 'source_commit_requires_client_tx_id', message: 'commit requires a clientTxId for idempotency' }, 400);
89
- }
90
- const parsed = changeSetSchema.safeParse({
91
- operations: body.operations,
92
- clientTxId: body.clientTxId,
93
- });
94
- if (!parsed.success) {
95
- return json({ error: 'source_commit_invalid', message: parsed.error.message }, 400);
96
- }
97
- const result = await adapter.commit(parsed.data);
98
- return json({ rows: result.rows });
99
- }
100
- if (body.type === 'events') {
101
- const page = await adapter.events(body.cursor ?? null, body.limit ?? 100);
102
- return json({
103
- events: page.events.map((event) => ({
104
- id: event.id,
105
- model: event.model,
106
- entityId: event.entityId,
107
- type: event.type,
108
- ...(event.data !== undefined && event.data !== null ? { data: event.data } : {}),
109
- ...(event.organizationId ? { organizationId: event.organizationId } : {}),
110
- ...(event.clientTxId ? { clientTxId: event.clientTxId } : {}),
111
- ...(event.occurredAt !== undefined && event.occurredAt !== null
112
- ? { occurredAt: event.occurredAt }
113
- : {}),
114
- })),
115
- ...(page.nextCursor !== null ? { nextCursor: page.nextCursor } : {}),
116
- });
117
- }
118
- return json({ error: 'unknown_source_request' }, 400);
119
- }
120
- async function readBody(request) {
121
- if (typeof request.text === 'function') {
122
- const rawBody = await request.text();
123
- return { rawBody, body: JSON.parse(rawBody) };
124
- }
125
- const body = (await request.json());
126
- return { rawBody: JSON.stringify(body), body };
127
- }
128
- function getHeader(request, name) {
129
- const headers = request.headers;
130
- if (!headers)
131
- return null;
132
- if (typeof headers.get === 'function') {
133
- return headers.get(name);
134
- }
135
- const record = headers;
136
- return record[name] ?? record[name.toLowerCase()] ?? null;
137
- }
138
- /**
139
- * Parse a `webhook-signature` header per the Standard Webhooks spec.
140
- * Values are space-delimited `<scheme>,<base64>` pairs (e.g.
141
- * `v1,abc== v1,def==` during a key rotation window). Returns the set
142
- * of `v1` signatures so the verifier can accept any of them.
143
- */
144
- function parseSignatureHeader(raw) {
145
- if (!raw)
146
- return [];
147
- const out = [];
148
- for (const part of raw.split(/\s+/)) {
149
- const trimmed = part.trim();
150
- if (!trimmed)
151
- continue;
152
- const commaAt = trimmed.indexOf(',');
153
- if (commaAt === -1)
154
- continue;
155
- const scheme = trimmed.slice(0, commaAt);
156
- const value = trimmed.slice(commaAt + 1);
157
- if (scheme === 'v1' && value.length > 0) {
158
- out.push(value);
159
- }
160
- }
161
- return out;
162
- }
163
- function bufferToBase64(buffer) {
164
- // Node + browsers both expose `btoa` on the global; we feed it
165
- // a binary string built from the byte view.
166
- let binary = '';
167
- const bytes = new Uint8Array(buffer);
168
- for (let i = 0; i < bytes.length; i++) {
169
- binary += String.fromCharCode(bytes[i]);
170
- }
171
- return btoa(binary);
172
- }
173
- async function hmacSha256Base64(apiKey, payload) {
174
- const crypto = globalThis.crypto?.subtle;
175
- if (!crypto) {
176
- throw new SourceSignatureError('source_signature_invalid', 'WebCrypto HMAC support is unavailable in this runtime');
177
- }
178
- const encoder = new TextEncoder();
179
- const key = await crypto.importKey('raw', encoder.encode(apiKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
180
- return bufferToBase64(await crypto.sign('HMAC', key, encoder.encode(payload)));
181
- }
182
- /**
183
- * Constant-time string equality. Used over `===` so a malicious
184
- * signature can't be probed byte-by-byte via timing differences.
185
- */
186
- function timingSafeEqual(expected, actual) {
187
- const max = Math.max(expected.length, actual.length);
188
- let diff = expected.length ^ actual.length;
189
- for (let i = 0; i < max; i++) {
190
- diff |= (expected.charCodeAt(i) || 0) ^ (actual.charCodeAt(i) || 0);
191
- }
192
- return diff === 0;
193
- }
194
- export async function signAbloSourceRequest(options) {
195
- const signedAt = options.timestamp ?? Math.floor(Date.now() / 1000);
196
- // Standard Webhooks signing input: `${msg_id}.${timestamp}.${payload}`
197
- const signature = await hmacSha256Base64(options.apiKey, `${options.messageId}.${signedAt}.${options.body}`);
198
- return {
199
- signedAt,
200
- signature,
201
- headers: {
202
- [ABLO_SOURCE_HEADERS.id]: options.messageId,
203
- [ABLO_SOURCE_HEADERS.timestamp]: String(signedAt),
204
- [ABLO_SOURCE_HEADERS.signature]: `v1,${signature}`,
205
- },
206
- };
207
- }
208
- export async function verifyAbloSourceRequest(options) {
209
- const messageId = getHeader(options.request, ABLO_SOURCE_HEADERS.id);
210
- if (!messageId) {
211
- throw new SourceSignatureError('source_id_missing', 'Missing webhook-id header');
212
- }
213
- const rawTimestamp = getHeader(options.request, ABLO_SOURCE_HEADERS.timestamp);
214
- if (!rawTimestamp) {
215
- throw new SourceSignatureError('source_timestamp_missing', 'Missing webhook-timestamp header');
216
- }
217
- const signedAt = Number(rawTimestamp);
218
- if (!Number.isFinite(signedAt)) {
219
- throw new SourceSignatureError('source_timestamp_invalid', 'Invalid webhook-timestamp header');
220
- }
221
- const toleranceMs = options.toleranceMs ?? DEFAULT_SIGNATURE_TOLERANCE_MS;
222
- const nowSeconds = Math.floor(Date.now() / 1000);
223
- const toleranceSeconds = Math.ceil(toleranceMs / 1000);
224
- if (Math.abs(nowSeconds - signedAt) > toleranceSeconds) {
225
- throw new SourceSignatureError('source_timestamp_expired', 'webhook-timestamp is outside the allowed clock-skew window');
226
- }
227
- const presented = parseSignatureHeader(getHeader(options.request, ABLO_SOURCE_HEADERS.signature));
228
- if (presented.length === 0) {
229
- throw new SourceSignatureError('source_signature_missing', 'Missing webhook-signature header');
230
- }
231
- const expected = await hmacSha256Base64(options.apiKey, `${messageId}.${signedAt}.${options.body}`);
232
- // Accept any presented signature that matches — supports key
233
- // rotation per the Standard Webhooks spec.
234
- const ok = presented.some((sig) => timingSafeEqual(expected, sig));
235
- if (!ok) {
236
- throw new SourceSignatureError('source_signature_invalid', 'Invalid webhook-signature');
237
- }
238
- return { messageId, signedAt };
239
- }
240
- async function resolveApiKey(apiKey, context) {
241
- if (!apiKey)
242
- return null;
243
- return typeof apiKey === 'function' ? apiKey(context) : apiKey;
244
- }
245
- /**
246
- * Map a wire request to its scope tag. Each request type corresponds
247
- * to one scope, so the function is total and exhaustive — adding a
248
- * new request type forces a new scope tag, which is the right design
249
- * pressure for keeping the scope vocabulary in sync with the wire.
4
+ * Pure re-exports only: the implementation lives in cohesive leaf modules so
5
+ * sibling source/* files (`pushQueue.ts`, `adapter.ts`, `contract.ts`, the ORM
6
+ * adapters) import the leaves directly instead of routing a runtime circular
7
+ * dependency through this barrel.
8
+ *
9
+ * - `types.ts` — shared wire/handler types + `sourceEventForOperation`
10
+ * - `signing.ts` Standard Webhooks request signing/verification
11
+ * - `factory.ts` the `abloSource()` / `dataSource()` endpoint factory
250
12
  */
251
- function scopeFor(body) {
252
- switch (body.type) {
253
- case 'load':
254
- return 'load';
255
- case 'list':
256
- return 'list';
257
- case 'commit':
258
- return 'commit';
259
- case 'events':
260
- return 'events';
261
- }
262
- }
263
- function normalizeListResult(result) {
264
- if (Array.isArray(result)) {
265
- return { rows: result };
266
- }
267
- const page = result;
268
- return page.nextCursor !== undefined
269
- ? { rows: page.rows, nextCursor: page.nextCursor }
270
- : { rows: page.rows };
271
- }
272
- function getModelHandlers(options, model) {
273
- const grouped = options.models?.[model];
274
- if (grouped)
275
- return grouped;
276
- const direct = options[model];
277
- return direct;
278
- }
279
- function sameModel(operations) {
280
- const first = operations[0]?.model;
281
- if (!first)
282
- return null;
283
- return operations.every((op) => op.model === first) ? first : null;
284
- }
285
- export function abloSource(options) {
286
- return async function handleAbloSource(request) {
287
- if (request.method !== 'POST') {
288
- return json({ error: 'method_not_allowed' }, 405);
289
- }
290
- let body;
291
- let rawBody;
292
- try {
293
- const parsed = await readBody(request);
294
- body = parsed.body;
295
- rawBody = parsed.rawBody;
296
- }
297
- catch {
298
- return json({ error: 'invalid_json' }, 400);
299
- }
300
- let signature = null;
301
- try {
302
- const apiKey = await resolveApiKey(options.apiKey, {
303
- request,
304
- body,
305
- rawBody,
306
- });
307
- if (!apiKey) {
308
- return json({
309
- error: 'source_api_key_missing',
310
- message: 'Data Source apiKey is required',
311
- }, 401);
312
- }
313
- signature = await verifyAbloSourceRequest({
314
- request,
315
- body: rawBody,
316
- apiKey,
317
- toleranceMs: options.signatureToleranceMs,
318
- });
319
- }
320
- catch (err) {
321
- if (err instanceof SourceSignatureError) {
322
- return json({ error: err.code, message: err.message }, 401);
323
- }
324
- throw err;
325
- }
326
- const auth = options.authorize
327
- ? await options.authorize({ request, body, rawBody })
328
- : undefined;
329
- // Per-key permission scope check. When `resolveScopes` is set,
330
- // the customer returns the operation set this key is allowed to
331
- // invoke; we enforce before any model handler runs.
332
- if (options.resolveScopes) {
333
- const required = scopeFor(body);
334
- const granted = await options.resolveScopes({ auth, request, body });
335
- const grantedSet = granted instanceof Set ? granted : new Set(granted);
336
- if (!grantedSet.has(required)) {
337
- return json({
338
- error: 'source_forbidden',
339
- required,
340
- granted: Array.from(grantedSet),
341
- }, 403);
342
- }
343
- }
344
- const context = {
345
- auth,
346
- request,
347
- messageId: signature?.messageId,
348
- signedAt: signature?.signedAt,
349
- ...(body.scope ? { scope: body.scope } : {}),
350
- };
351
- // Adapter path: when an ORM adapter is configured it serves every operation,
352
- // consumed at this generic layer (rows are JSON on the wire), so no per-model
353
- // handler lookup and no typed↔generic boundary.
354
- if (options.adapter) {
355
- return handleViaAdapter(options.adapter, body, context.scope);
356
- }
357
- if (body.type === 'load') {
358
- const handlers = getModelHandlers(options, body.model);
359
- if (!handlers?.load) {
360
- return json({ error: 'source_load_not_configured', model: body.model }, 404);
361
- }
362
- const row = await handlers.load({ id: body.id, context });
363
- return json({ row });
364
- }
365
- if (body.type === 'list') {
366
- const handlers = getModelHandlers(options, body.model);
367
- if (!handlers?.list) {
368
- return json({ error: 'source_list_not_configured', model: body.model }, 404);
369
- }
370
- const result = await handlers.list({ query: body.query ?? {}, context });
371
- const normalized = normalizeListResult(result);
372
- return json(normalized);
373
- }
374
- if (body.type === 'commit') {
375
- if (options.commit) {
376
- const result = await options.commit({
377
- operations: body.operations,
378
- clientTxId: body.clientTxId,
379
- context,
380
- });
381
- return json(result);
382
- }
383
- const model = body.model ?? sameModel(body.operations);
384
- if (!model) {
385
- return json({ error: 'source_commit_requires_single_model' }, 400);
386
- }
387
- const handlers = getModelHandlers(options, model);
388
- if (!handlers?.commit) {
389
- return json({ error: 'source_commit_not_configured', model }, 404);
390
- }
391
- const result = await handlers.commit({
392
- operations: body.operations,
393
- clientTxId: body.clientTxId,
394
- context,
395
- });
396
- return json(result);
397
- }
398
- if (body.type === 'events') {
399
- if (!options.events) {
400
- return json({ error: 'source_events_not_configured' }, 404);
401
- }
402
- const result = await options.events({
403
- cursor: body.cursor,
404
- limit: body.limit,
405
- context,
406
- });
407
- return json({
408
- events: result.events,
409
- ...(result.nextCursor !== undefined
410
- ? { nextCursor: result.nextCursor }
411
- : {}),
412
- });
413
- }
414
- return json({ error: 'unknown_source_request' }, 400);
415
- };
416
- }
417
- export function dataSource(options) {
418
- return abloSource(options);
419
- }
13
+ export { sourceEventForOperation, } from './types.js';
14
+ export { ABLO_SOURCE_HEADERS, SourceSignatureError, signAbloSourceRequest, verifyAbloSourceRequest, } from './signing.js';
15
+ export { abloSource, dataSource, } from './factory.js';
420
16
  export { createPushQueue, InMemoryPushQueueStorage, STANDARD_WEBHOOKS_RETRY_SCHEDULE, } from './pushQueue.js';
421
17
  // ── Reverse-channel connector (outbound transport for the commit/load/list leg) ──
422
18
  // The dial-out counterpart to `createPushQueue`. Lets a customer serve Data
@@ -424,6 +20,8 @@ export { createPushQueue, InMemoryPushQueueStorage, STANDARD_WEBHOOKS_RETRY_SCHE
424
20
  // public inbound URL — see `connector-protocol.ts`.
425
21
  export { createSourceConnector, DEFAULT_RECONNECT_SCHEDULE, } from './connector.js';
426
22
  export { SOURCE_CONNECTOR_PROTOCOL_VERSION, SOURCE_CONNECTOR_WS_PATH, WS_SOURCE_SUBPROTOCOL, sourceConnectorSubprotocols, encodeFrame, decodeFrame, ConnectorProtocolError, connectorFrameSchema, } from './connector-protocol.js';
23
+ // ── Data Source adapter interface (Zod contract + one interface, per-ORM packages) ──
24
+ export {} from './adapter.js';
427
25
  export { operationSchema, operationTypeSchema, changeSetSchema, outboxEventSchema, eventsPageSchema, migrationSchema, adapterCapabilitiesSchema, } from './contract.js';
428
26
  export { prismaDataSource } from './adapters/prisma.js';
429
27
  export { adapterTableMigrations } from './migrations.js';
@@ -21,7 +21,7 @@
21
21
  * — not pre-built.
22
22
  */
23
23
  import type { SchemaRecord } from '../schema/schema.js';
24
- import { type DataSourceOptions } from './index.js';
24
+ import { type DataSourceOptions } from './factory.js';
25
25
  /**
26
26
  * Next options ARE the core options — the `adapter` field lives on the core
27
27
  * handler now, so there is no bridging, no cast, and no per-model-typed boundary
@@ -20,7 +20,7 @@
20
20
  * `dataSource()` directly; Hono/Express are the same one-liner and land on demand
21
21
  * — not pre-built.
22
22
  */
23
- import { dataSource } from './index.js';
23
+ import { dataSource } from './factory.js';
24
24
  export function dataSourceNext(options) {
25
25
  return { POST: dataSource(options) };
26
26
  }
@@ -20,7 +20,7 @@
20
20
  * process customers, a SQL implementation against the customer's own
21
21
  * outbox table for production.
22
22
  */
23
- import { type SourceEvent } from './index.js';
23
+ import type { SourceEvent } from './types.js';
24
24
  export interface PushQueueItem {
25
25
  readonly id: string;
26
26
  readonly events: readonly SourceEvent[];
@@ -20,7 +20,7 @@
20
20
  * process customers, a SQL implementation against the customer's own
21
21
  * outbox table for production.
22
22
  */
23
- import { ABLO_SOURCE_HEADERS, signAbloSourceRequest, } from './index.js';
23
+ import { ABLO_SOURCE_HEADERS, signAbloSourceRequest } from './signing.js';
24
24
  /**
25
25
  * Standard Webhooks retry schedule. Index = attempt number; value =
26
26
  * delay-ms after the previous attempt. After the last entry, items
@@ -133,12 +133,15 @@ export function createPushQueue(options) {
133
133
  }
134
134
  async function reschedule(item, error) {
135
135
  const nextAttempt = item.attempts + 1;
136
- if (nextAttempt >= schedule.length) {
136
+ // Off the end of the backoff schedule (same check as the old
137
+ // `nextAttempt >= schedule.length`) — dead-letter the item.
138
+ const backoff = schedule[nextAttempt];
139
+ if (backoff === undefined) {
137
140
  await options.storage.markDlq(item.id, error);
138
141
  options.onError?.(item, new Error(error));
139
142
  return;
140
143
  }
141
- const delay = applyJitter(schedule[nextAttempt], jitter);
144
+ const delay = applyJitter(backoff, jitter);
142
145
  await options.storage.reschedule(item.id, now() + delay, error);
143
146
  }
144
147
  }
@@ -0,0 +1,59 @@
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
+ export interface SourceSignatureOptions {
11
+ readonly apiKey: string;
12
+ readonly body: string;
13
+ /**
14
+ * Unique message id (`webhook-id` per the Standard Webhooks spec).
15
+ * Required: it goes into the HMAC input for replay defense, and
16
+ * receivers may dedupe by it.
17
+ */
18
+ readonly messageId: string;
19
+ /**
20
+ * Unix timestamp in seconds. Defaults to the current time.
21
+ */
22
+ readonly timestamp?: number;
23
+ }
24
+ export interface SourceSignatureVerificationOptions {
25
+ readonly request: Request;
26
+ readonly body: string;
27
+ readonly apiKey: string;
28
+ readonly toleranceMs?: number;
29
+ }
30
+ export interface SourceSignatureVerificationResult {
31
+ readonly messageId: string;
32
+ readonly signedAt: number;
33
+ }
34
+ /**
35
+ * HTTP headers used on signed source requests. Conforms to the
36
+ * Standard Webhooks specification (https://www.standardwebhooks.com/)
37
+ * so customer code can verify our signatures with any of the official
38
+ * libraries (svix, standardwebhooks, hookdeck, etc.) — no Ablo-
39
+ * specific verifier required.
40
+ */
41
+ export declare const ABLO_SOURCE_HEADERS: {
42
+ readonly signature: "webhook-signature";
43
+ readonly timestamp: "webhook-timestamp";
44
+ readonly id: "webhook-id";
45
+ readonly idempotencyKey: "Idempotency-Key";
46
+ };
47
+ export declare class SourceSignatureError extends Error {
48
+ readonly code: 'source_signature_missing' | 'source_id_missing' | 'source_timestamp_missing' | 'source_timestamp_invalid' | 'source_timestamp_expired' | 'source_signature_invalid' | 'source_forbidden';
49
+ constructor(code: SourceSignatureError['code'], message: string);
50
+ }
51
+ export declare function signAbloSourceRequest(options: SourceSignatureOptions): Promise<{
52
+ readonly headers: Record<string, string>;
53
+ readonly signedAt: number;
54
+ readonly signature: string;
55
+ }>;
56
+ export declare function verifyAbloSourceRequest(options: SourceSignatureVerificationOptions): Promise<SourceSignatureVerificationResult>;
57
+ export type DataSourceSignatureOptions = SourceSignatureOptions;
58
+ export type DataSourceSignatureVerificationOptions = SourceSignatureVerificationOptions;
59
+ export type DataSourceSignatureVerificationResult = SourceSignatureVerificationResult;