@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.
- package/AGENTS.md +5 -3
- package/CHANGELOG.md +20 -0
- package/README.md +3 -3
- package/dist/BaseSyncedStore.d.ts +72 -195
- package/dist/BaseSyncedStore.js +240 -625
- package/dist/Database.d.ts +8 -9
- package/dist/Database.js +9 -14
- package/dist/LazyReferenceCollection.js +5 -0
- package/dist/Model.js +6 -8
- package/dist/ModelRegistry.d.ts +25 -6
- package/dist/ModelRegistry.js +12 -6
- package/dist/NetworkMonitor.js +1 -1
- package/dist/ObjectPool.js +9 -3
- package/dist/SyncClient.d.ts +15 -5
- package/dist/SyncClient.js +152 -66
- package/dist/SyncEngineContext.d.ts +1 -3
- package/dist/SyncEngineContext.js +1 -2
- package/dist/agent/Agent.d.ts +21 -8
- package/dist/agent/Agent.js +26 -13
- package/dist/ai-sdk/coordination-context.js +9 -3
- package/dist/ai-sdk/wrap.d.ts +1 -1
- package/dist/ai-sdk/wrap.js +1 -1
- package/dist/auth/credentialPolicy.d.ts +10 -2
- package/dist/auth/credentialSource.d.ts +1 -13
- package/dist/auth/credentialSource.js +6 -13
- package/dist/auth/index.js +3 -3
- package/dist/cli.cjs +328 -262
- package/dist/client/Ablo.d.ts +35 -764
- package/dist/client/Ablo.js +64 -688
- package/dist/client/ApiClient.d.ts +14 -1
- package/dist/client/ApiClient.js +63 -9
- package/dist/client/auth.d.ts +8 -17
- package/dist/client/auth.js +42 -6
- package/dist/client/consoleLogger.d.ts +36 -0
- package/dist/client/consoleLogger.js +45 -0
- package/dist/client/createInternalComponents.js +1 -1
- package/dist/client/createModelProxy.js +10 -6
- package/dist/client/credentialEndpoint.d.ts +63 -0
- package/dist/client/credentialEndpoint.js +87 -0
- package/dist/client/hostedEndpoints.d.ts +24 -0
- package/dist/client/hostedEndpoints.js +24 -0
- package/dist/client/httpClient.d.ts +1 -1
- package/dist/client/identity.js +2 -1
- package/dist/client/modelRegistration.d.ts +14 -0
- package/dist/client/modelRegistration.js +321 -0
- package/dist/client/options.d.ts +396 -0
- package/dist/client/options.js +10 -0
- package/dist/client/resourceTypes.d.ts +344 -0
- package/dist/client/resourceTypes.js +13 -0
- package/dist/client/schemaConfig.d.ts +56 -0
- package/dist/client/schemaConfig.js +188 -0
- package/dist/client/sessionMint.d.ts +1 -1
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/client/wsMutationExecutor.d.ts +33 -0
- package/dist/client/wsMutationExecutor.js +77 -0
- package/dist/context.js +0 -3
- package/dist/coordination/index.d.ts +8 -1
- package/dist/coordination/index.js +24 -1
- package/dist/coordination/trace.d.ts +0 -1
- package/dist/coordination/trace.js +0 -1
- package/dist/core/DatabaseManager.d.ts +0 -1
- package/dist/core/DatabaseManager.js +26 -24
- package/dist/core/QueryProcessor.js +7 -8
- package/dist/core/QueryView.d.ts +14 -2
- package/dist/core/QueryView.js +4 -5
- package/dist/core/StoreManager.d.ts +2 -2
- package/dist/core/StoreManager.js +5 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/openIDBWithTimeout.js +15 -11
- package/dist/core/query-utils.js +1 -1
- package/dist/core/storeContract.d.ts +143 -0
- package/dist/core/storeContract.js +16 -0
- package/dist/errorCodes.d.ts +11 -2
- package/dist/errorCodes.js +155 -137
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +14 -1
- package/dist/index.js +5 -0
- package/dist/interfaces/index.d.ts +6 -20
- package/dist/keys/index.js +18 -9
- package/dist/mutators/UndoManager.js +6 -1
- package/dist/mutators/defineMutators.d.ts +1 -3
- package/dist/policy/types.d.ts +1 -1
- package/dist/query/client.d.ts +11 -0
- package/dist/query/client.js +81 -52
- package/dist/react/AbloProvider.d.ts +6 -10
- package/dist/react/AbloProvider.js +22 -15
- package/dist/react/context.d.ts +2 -118
- package/dist/react/useAblo.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useMutators.js +7 -1
- package/dist/react/useReactive.js +0 -1
- package/dist/react/useUndoScope.js +2 -2
- package/dist/schema/ddlLock.d.ts +39 -0
- package/dist/schema/ddlLock.js +51 -0
- package/dist/schema/field.js +1 -1
- package/dist/schema/index.d.ts +4 -3
- package/dist/schema/index.js +15 -5
- package/dist/schema/model.d.ts +3 -3
- package/dist/schema/model.js +2 -2
- package/dist/schema/openapi.js +5 -1
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -0
- package/dist/schema/residency.d.ts +35 -0
- package/dist/schema/residency.js +27 -0
- package/dist/schema/roles.d.ts +25 -0
- package/dist/schema/roles.js +37 -0
- package/dist/schema/schema.d.ts +3 -3
- package/dist/schema/schema.js +11 -7
- package/dist/schema/serialize.d.ts +8 -8
- package/dist/schema/sync-delta-row.d.ts +2 -2
- package/dist/schema/sync-delta-row.js +2 -2
- package/dist/server/storage-mode.d.ts +8 -1
- package/dist/server/storage-mode.js +14 -4
- package/dist/source/adapter.d.ts +1 -1
- package/dist/source/adapters/drizzle.js +4 -3
- package/dist/source/adapters/kysely.js +4 -3
- package/dist/source/adapters/memory.js +1 -1
- package/dist/source/adapters/prisma.js +5 -4
- package/dist/source/conformance.js +10 -8
- package/dist/source/connector.js +4 -2
- package/dist/source/factory.d.ts +106 -0
- package/dist/source/factory.js +273 -0
- package/dist/source/index.d.ts +11 -461
- package/dist/source/index.js +14 -416
- package/dist/source/next.d.ts +1 -1
- package/dist/source/next.js +1 -1
- package/dist/source/pushQueue.d.ts +1 -1
- package/dist/source/pushQueue.js +6 -3
- package/dist/source/signing.d.ts +59 -0
- package/dist/source/signing.js +142 -0
- package/dist/source/types.d.ts +334 -0
- package/dist/source/types.js +44 -0
- package/dist/stores/ObjectStore.d.ts +1 -1
- package/dist/stores/ObjectStore.js +23 -23
- package/dist/stores/SyncActionStore.d.ts +1 -1
- package/dist/stores/SyncActionStore.js +70 -35
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/dist/sync/AreaOfInterestManager.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.js +1 -1
- package/dist/sync/BootstrapHelper.d.ts +1 -3
- package/dist/sync/BootstrapHelper.js +29 -8
- package/dist/sync/ConnectionManager.d.ts +8 -0
- package/dist/sync/ConnectionManager.js +19 -4
- package/dist/sync/HydrationCoordinator.d.ts +12 -0
- package/dist/sync/HydrationCoordinator.js +25 -5
- package/dist/sync/NetworkProbe.d.ts +3 -2
- package/dist/sync/NetworkProbe.js +7 -6
- package/dist/sync/SyncWebSocket.d.ts +71 -120
- package/dist/sync/SyncWebSocket.js +324 -630
- package/dist/sync/awaitClaimGrant.js +27 -19
- package/dist/sync/bootstrapApply.d.ts +60 -0
- package/dist/sync/bootstrapApply.js +65 -0
- package/dist/sync/commitFrames.d.ts +43 -0
- package/dist/sync/commitFrames.js +94 -0
- package/dist/sync/createClaimStream.js +3 -2
- package/dist/sync/createSnapshot.d.ts +1 -3
- package/dist/sync/credentialLifecycle.d.ts +175 -0
- package/dist/sync/credentialLifecycle.js +329 -0
- package/dist/sync/deltaPipeline.d.ts +111 -0
- package/dist/sync/deltaPipeline.js +262 -0
- package/dist/sync/groupChange.d.ts +111 -0
- package/dist/sync/groupChange.js +246 -0
- package/dist/sync/heartbeat.d.ts +62 -0
- package/dist/sync/heartbeat.js +91 -0
- package/dist/sync/participants.d.ts +8 -8
- package/dist/sync/syncCursor.d.ts +44 -0
- package/dist/sync/syncCursor.js +59 -0
- package/dist/sync/syncPlan.d.ts +62 -0
- package/dist/sync/syncPlan.js +51 -0
- package/dist/sync/wsFrameHandlers.d.ts +117 -0
- package/dist/sync/wsFrameHandlers.js +378 -0
- package/dist/testing/fixtures/bootstrap.d.ts +9 -11
- package/dist/testing/fixtures/deltas.d.ts +1 -1
- package/dist/testing/fixtures/models.js +1 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
- package/dist/testing/helpers/sync-engine-harness.js +2 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
- package/dist/testing/mocks/MockSyncContext.js +1 -32
- package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
- package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
- package/dist/transactions/TransactionQueue.d.ts +20 -56
- package/dist/transactions/TransactionQueue.js +101 -487
- package/dist/transactions/TransactionStore.d.ts +18 -0
- package/dist/transactions/TransactionStore.js +51 -0
- package/dist/transactions/coalesceRules.d.ts +34 -0
- package/dist/transactions/coalesceRules.js +117 -0
- package/dist/transactions/commitPayload.d.ts +134 -0
- package/dist/transactions/commitPayload.js +152 -0
- package/dist/transactions/deltaConfirmation.d.ts +60 -0
- package/dist/transactions/deltaConfirmation.js +223 -0
- package/dist/transactions/optimistic.d.ts +24 -0
- package/dist/transactions/optimistic.js +45 -0
- package/dist/transactions/persistedReplay.d.ts +93 -0
- package/dist/transactions/persistedReplay.js +105 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +0 -5
- package/dist/types/modelData.d.ts +12 -0
- package/dist/types/modelData.js +11 -0
- package/dist/types/participant.d.ts +21 -0
- package/dist/types/participant.js +12 -0
- package/dist/types/streams.d.ts +8 -14
- package/dist/utils/mobx-setup.d.ts +1 -1
- package/dist/utils/mobx-setup.js +2 -2
- package/dist/wire/errorEnvelope.d.ts +11 -2
- package/dist/wire/errorEnvelope.js +14 -2
- package/dist/wire/frames.d.ts +90 -0
- package/dist/wire/frames.js +55 -1
- package/dist/wire/index.d.ts +4 -1
- package/dist/wire/index.js +20 -1
- package/dist/wire/protocol.d.ts +45 -0
- package/dist/wire/protocol.js +45 -0
- package/dist/wire/protocolVersion.d.ts +56 -0
- package/dist/wire/protocolVersion.js +63 -0
- package/docs/api-keys.md +4 -3
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/identity.md +4 -4
- package/docs/integration-guide.md +1 -1
- package/docs/react.md +1 -1
- package/docs/sessions.md +5 -7
- package/package.json +15 -13
- package/dist/client/index.d.ts +0 -36
- package/dist/client/index.js +0 -33
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -12
- package/dist/interfaces/headless.d.ts +0 -95
- package/dist/interfaces/headless.js +0 -41
- package/dist/query/index.d.ts +0 -6
- package/dist/query/index.js +0 -5
- package/dist/realtime/index.d.ts +0 -10
- package/dist/realtime/index.js +0 -9
- package/dist/schema/plane.d.ts +0 -23
- package/dist/schema/plane.js +0 -19
- package/dist/server/next.d.ts +0 -51
- package/dist/server/next.js +0 -47
- package/dist/sync/OfflineFlush.d.ts +0 -9
- package/dist/sync/OfflineFlush.js +0 -22
- package/dist/sync/OfflineTransactionStore.d.ts +0 -37
- package/dist/sync/OfflineTransactionStore.js +0 -263
- package/dist/transactions/index.d.ts +0 -16
- package/dist/transactions/index.js +0 -7
package/dist/source/index.js
CHANGED
|
@@ -1,422 +1,18 @@
|
|
|
1
|
-
import { AbloValidationError } from '../errors.js';
|
|
2
|
-
import { changeSetSchema } from './contract.js';
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
* the SAME transaction as their app-row mutation.
|
|
2
|
+
* `@abloatai/ablo/source` — the Data Source barrel.
|
|
6
3
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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';
|
package/dist/source/next.d.ts
CHANGED
|
@@ -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 './
|
|
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
|
package/dist/source/next.js
CHANGED
|
@@ -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 './
|
|
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 {
|
|
23
|
+
import type { SourceEvent } from './types.js';
|
|
24
24
|
export interface PushQueueItem {
|
|
25
25
|
readonly id: string;
|
|
26
26
|
readonly events: readonly SourceEvent[];
|
package/dist/source/pushQueue.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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;
|