@abloatai/ablo 0.8.0 → 0.9.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/CHANGELOG.md +40 -1
- package/README.md +32 -27
- package/dist/BaseSyncedStore.d.ts +73 -0
- package/dist/BaseSyncedStore.js +172 -2
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/agent/session.js +3 -3
- package/dist/ai-sdk/coordination-context.js +4 -0
- package/dist/ai-sdk/index.d.ts +56 -47
- package/dist/ai-sdk/index.js +56 -47
- package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
- package/dist/ai-sdk/intent-broadcast.js +11 -4
- package/dist/ai-sdk/wrap.d.ts +14 -11
- package/dist/ai-sdk/wrap.js +11 -13
- package/dist/auth/credentialSource.d.ts +34 -0
- package/dist/auth/credentialSource.js +63 -0
- package/dist/auth/index.d.ts +2 -22
- package/dist/auth/index.js +4 -42
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +160 -42
- package/dist/client/Ablo.js +145 -75
- package/dist/client/ApiClient.d.ts +20 -4
- package/dist/client/ApiClient.js +166 -28
- package/dist/client/auth.d.ts +14 -5
- package/dist/client/auth.js +60 -7
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +130 -66
- package/dist/client/createModelProxy.js +152 -49
- package/dist/client/httpClient.d.ts +71 -0
- package/dist/client/httpClient.js +69 -0
- package/dist/client/identity.d.ts +2 -6
- package/dist/client/identity.js +49 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +3 -3
- package/dist/client/registerDataSource.js +11 -9
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/core/DatabaseManager.js +30 -2
- package/dist/core/openIDBWithTimeout.d.ts +36 -0
- package/dist/core/openIDBWithTimeout.js +88 -1
- package/dist/errorCodes.d.ts +70 -1
- package/dist/errorCodes.js +108 -9
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +72 -22
- package/dist/index.d.ts +17 -8
- package/dist/index.js +15 -6
- package/dist/keys/index.d.ts +16 -1
- package/dist/keys/index.js +26 -6
- package/dist/mutators/UndoManager.d.ts +86 -50
- package/dist/mutators/UndoManager.js +129 -22
- package/dist/mutators/inverseOp.d.ts +129 -0
- package/dist/mutators/inverseOp.js +74 -0
- package/dist/mutators/readerActions.d.ts +1 -1
- package/dist/mutators/undoApply.d.ts +42 -0
- package/dist/mutators/undoApply.js +143 -0
- package/dist/query/client.d.ts +10 -9
- package/dist/query/client.js +3 -6
- package/dist/react/AbloProvider.d.ts +23 -126
- package/dist/react/AbloProvider.js +62 -199
- package/dist/react/useAblo.d.ts +2 -2
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/schema/ddl.d.ts +26 -3
- package/dist/schema/ddl.js +152 -4
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.js +12 -0
- package/dist/schema/model.d.ts +11 -0
- package/dist/schema/model.js +2 -0
- package/dist/schema/openapi.d.ts +28 -0
- package/dist/schema/openapi.js +118 -0
- package/dist/schema/plane.d.ts +23 -0
- package/dist/schema/plane.js +19 -0
- package/dist/schema/relation.d.ts +20 -0
- package/dist/schema/serialize.d.ts +4 -0
- package/dist/schema/serialize.js +4 -0
- package/dist/schema/sync-delta-row.d.ts +157 -0
- package/dist/schema/sync-delta-row.js +102 -0
- package/dist/schema/sync-delta-wire.d.ts +180 -0
- package/dist/schema/sync-delta-wire.js +102 -0
- package/dist/server/adapter.d.ts +156 -0
- package/dist/server/adapter.js +19 -0
- package/dist/server/commit.d.ts +82 -0
- package/dist/server/commit.js +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +1 -0
- package/dist/server/next.d.ts +51 -0
- package/dist/server/next.js +47 -0
- package/dist/server/read-config.d.ts +60 -0
- package/dist/server/read-config.js +8 -0
- package/dist/server/storage-mode.d.ts +17 -0
- package/dist/server/storage-mode.js +12 -0
- package/dist/source/adapter.d.ts +59 -0
- package/dist/source/adapter.js +19 -0
- package/dist/source/adapters/drizzle.d.ts +34 -0
- package/dist/source/adapters/drizzle.js +147 -0
- package/dist/source/adapters/memory.d.ts +12 -0
- package/dist/source/adapters/memory.js +114 -0
- package/dist/source/adapters/prisma.d.ts +57 -0
- package/dist/source/adapters/prisma.js +199 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +143 -0
- package/dist/source/contract.js +98 -0
- package/dist/source/index.d.ts +61 -10
- package/dist/source/index.js +98 -0
- package/dist/source/next.d.ts +33 -0
- package/dist/source/next.js +26 -0
- package/dist/sync/BootstrapHelper.d.ts +10 -0
- package/dist/sync/BootstrapHelper.js +10 -15
- package/dist/sync/ConnectionManager.d.ts +55 -1
- package/dist/sync/ConnectionManager.js +155 -16
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +238 -39
- package/dist/sync/NetworkProbe.d.ts +58 -24
- package/dist/sync/NetworkProbe.js +118 -42
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +70 -36
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/types/streams.d.ts +9 -0
- package/dist/utils/mobx-setup.js +1 -0
- package/dist/webhooks/events.d.ts +38 -0
- package/dist/webhooks/events.js +40 -0
- package/dist/webhooks/index.d.ts +10 -0
- package/dist/webhooks/index.js +10 -0
- package/dist/wire/errorEnvelope.d.ts +34 -0
- package/dist/wire/errorEnvelope.js +86 -0
- package/dist/wire/frames.d.ts +119 -0
- package/dist/wire/frames.js +1 -0
- package/dist/wire/index.d.ts +24 -0
- package/dist/wire/index.js +21 -0
- package/dist/wire/listEnvelope.d.ts +45 -0
- package/dist/wire/listEnvelope.js +17 -0
- package/docs/api.md +47 -44
- package/docs/cli.md +44 -44
- package/docs/client-behavior.md +30 -30
- package/docs/coordination.md +33 -36
- package/docs/data-sources.md +35 -15
- package/docs/examples/agent-human.md +45 -43
- package/docs/examples/ai-sdk-tool.md +20 -16
- package/docs/examples/existing-python-backend.md +16 -12
- package/docs/examples/nextjs.md +14 -12
- package/docs/examples/scoped-agent.md +1 -1
- package/docs/examples/server-agent.md +24 -21
- package/docs/guarantees.md +15 -13
- package/docs/index.md +1 -1
- package/docs/integration-guide.md +30 -30
- package/docs/interaction-model.md +19 -23
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +2 -2
- package/docs/mcp.md +6 -6
- package/docs/quickstart.md +41 -31
- package/docs/react.md +13 -9
- package/docs/schema-contract.md +12 -10
- package/docs/the-loop.md +21 -0
- package/examples/data-source/README.md +4 -5
- package/examples/data-source/customer-server.ts +27 -25
- package/llms.txt +28 -5
- package/package.json +43 -3
package/dist/errors.js
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Both work on every subclass.
|
|
20
20
|
*/
|
|
21
|
-
import {
|
|
22
|
-
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import { errorCodeSpec, classifyRecovery } from './errorCodes.js';
|
|
23
|
+
export { ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, classifyRecovery, recoveryClassSchema, RECOVERY_CLASSES, } from './errorCodes.js';
|
|
23
24
|
// ── AbloError hierarchy — the typed error surface ────────────────────
|
|
24
25
|
/** Common shape for all errors thrown by this SDK. */
|
|
25
26
|
export class AbloError extends Error {
|
|
@@ -238,15 +239,22 @@ export class SyncSessionError extends AbloAuthenticationError {
|
|
|
238
239
|
* Check if an HTTP response status indicates a session error
|
|
239
240
|
*/
|
|
240
241
|
static isSessionErrorResponse(status, body) {
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
242
|
+
// "Should this response sign the user out?" — TRUE only for a genuine
|
|
243
|
+
// expiry of the LONG-LIVED login (`recovery: 'session_expiry'`). Decided
|
|
244
|
+
// via the closed recovery taxonomy rather than a hardcoded code list, so
|
|
245
|
+
// the access-vs-session split lives in one place (errorCodes.ts). This is
|
|
246
|
+
// behaviourally identical to the old `session_expired || jwt_expired` list.
|
|
247
|
+
//
|
|
248
|
+
// Deliberately NOT true for `access_credential_expiry` (`apikey_expired` —
|
|
249
|
+
// the Stripe-style ephemeral key): an expired `ek_`/`rk_` is re-mintable
|
|
250
|
+
// from the still-valid login and must NOT log the user out — the connection
|
|
251
|
+
// layer silently re-mints instead. Likewise NOT true for `auth_blocked` /
|
|
252
|
+
// `permission` failures (api_key_required, jwt_issuer_untrusted, 403s):
|
|
253
|
+
// re-auth re-mints the same rejected credential and loops ("flash then
|
|
254
|
+
// bounce to /signin").
|
|
247
255
|
const code = extractWireCode(body);
|
|
248
256
|
if (code) {
|
|
249
|
-
return code === '
|
|
257
|
+
return classifyRecovery(code) === 'session_expiry';
|
|
250
258
|
}
|
|
251
259
|
// No structured code (bare body, non-Ablo proxy response): a 401 is taken as
|
|
252
260
|
// expiry — the historical default that drives re-auth — while a 403 is a
|
|
@@ -254,6 +262,50 @@ export class SyncSessionError extends AbloAuthenticationError {
|
|
|
254
262
|
return status === 401;
|
|
255
263
|
}
|
|
256
264
|
}
|
|
265
|
+
// ── HTTP → class mapping ──────────────────────────────────────────────
|
|
266
|
+
const OptionalWireStringSchema = z.preprocess((value) => (typeof value === 'string' ? value : undefined), z.string().optional());
|
|
267
|
+
const RequiredCapabilityWireSchema = z
|
|
268
|
+
.object({
|
|
269
|
+
scope: z.string(),
|
|
270
|
+
constraints: z
|
|
271
|
+
.record(z.string(), z.union([z.array(z.string()), z.string()]))
|
|
272
|
+
.optional(),
|
|
273
|
+
issuer: OptionalWireStringSchema,
|
|
274
|
+
ttlSeconds: z
|
|
275
|
+
.preprocess((value) => (typeof value === 'number' ? value : undefined), z.number().optional()),
|
|
276
|
+
nonce: OptionalWireStringSchema,
|
|
277
|
+
})
|
|
278
|
+
.passthrough();
|
|
279
|
+
const NestedErrorShapeSchema = z
|
|
280
|
+
.object({
|
|
281
|
+
code: OptionalWireStringSchema,
|
|
282
|
+
message: OptionalWireStringSchema,
|
|
283
|
+
field: OptionalWireStringSchema,
|
|
284
|
+
requiredCapability: RequiredCapabilityWireSchema.optional().catch(undefined),
|
|
285
|
+
})
|
|
286
|
+
.passthrough();
|
|
287
|
+
const ErrorFieldSchema = z
|
|
288
|
+
.preprocess((value) => typeof value === 'string' || (typeof value === 'object' && value !== null)
|
|
289
|
+
? value
|
|
290
|
+
: undefined, z.union([z.string(), NestedErrorShapeSchema]).optional())
|
|
291
|
+
.catch(undefined);
|
|
292
|
+
const ErrorBodyShapeSchema = z
|
|
293
|
+
.object({
|
|
294
|
+
/** Legacy: `error` was a flat code string on older endpoints. Newer
|
|
295
|
+
* endpoints (CommitReceipt) carry `error` as a nested object. */
|
|
296
|
+
error: ErrorFieldSchema,
|
|
297
|
+
code: OptionalWireStringSchema,
|
|
298
|
+
reason: OptionalWireStringSchema,
|
|
299
|
+
message: OptionalWireStringSchema,
|
|
300
|
+
requiredCapability: RequiredCapabilityWireSchema.optional().catch(undefined),
|
|
301
|
+
})
|
|
302
|
+
.passthrough();
|
|
303
|
+
function parseErrorBodyShape(body) {
|
|
304
|
+
if (typeof body !== 'object' || body === null)
|
|
305
|
+
return {};
|
|
306
|
+
const parsed = ErrorBodyShapeSchema.safeParse(body);
|
|
307
|
+
return parsed.success ? parsed.data : {};
|
|
308
|
+
}
|
|
257
309
|
/**
|
|
258
310
|
* Coerce ANY thrown value into an {@link AbloError} — the last-line guarantee
|
|
259
311
|
* that an SDK consumer never catches an untagged error. An already-typed
|
|
@@ -340,7 +392,7 @@ export function errorFromWire(message, opts = {}) {
|
|
|
340
392
|
* frame transports) after extracting code/message from the HTTP body.
|
|
341
393
|
*/
|
|
342
394
|
export function translateHttpError(status, body, requestId) {
|
|
343
|
-
const parsed =
|
|
395
|
+
const parsed = parseErrorBodyShape(body);
|
|
344
396
|
const nested = parsed.error != null && typeof parsed.error === 'object'
|
|
345
397
|
? parsed.error
|
|
346
398
|
: undefined;
|
|
@@ -363,16 +415,14 @@ export function translateHttpError(status, body, requestId) {
|
|
|
363
415
|
* of emitting a code-less error.
|
|
364
416
|
*/
|
|
365
417
|
export function hasWireCode(body) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const b = body;
|
|
369
|
-
if (typeof b.code === 'string')
|
|
418
|
+
const parsed = parseErrorBodyShape(body);
|
|
419
|
+
if (typeof parsed.code === 'string')
|
|
370
420
|
return true;
|
|
371
|
-
if (typeof
|
|
421
|
+
if (typeof parsed.error === 'string')
|
|
372
422
|
return true;
|
|
373
|
-
return (typeof
|
|
374
|
-
|
|
375
|
-
typeof
|
|
423
|
+
return (typeof parsed.error === 'object' &&
|
|
424
|
+
parsed.error !== null &&
|
|
425
|
+
typeof parsed.error.code === 'string');
|
|
376
426
|
}
|
|
377
427
|
/**
|
|
378
428
|
* Extract the canonical error `code` from a raw HTTP error body STRING — the
|
|
@@ -392,12 +442,12 @@ export function extractWireCode(body) {
|
|
|
392
442
|
}
|
|
393
443
|
if (typeof parsed !== 'object' || parsed === null)
|
|
394
444
|
return undefined;
|
|
395
|
-
const b = parsed;
|
|
445
|
+
const b = parseErrorBodyShape(parsed);
|
|
396
446
|
if (typeof b.code === 'string')
|
|
397
447
|
return b.code;
|
|
398
|
-
if (typeof b.error === '
|
|
399
|
-
b.error
|
|
400
|
-
|
|
448
|
+
if (typeof b.error === 'string')
|
|
449
|
+
return b.error;
|
|
450
|
+
if (typeof b.error === 'object' && b.error !== null && typeof b.error.code === 'string') {
|
|
401
451
|
return b.error.code;
|
|
402
452
|
}
|
|
403
453
|
return undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* import Ablo from '@abloatai/ablo';
|
|
6
6
|
*
|
|
7
7
|
* const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
8
|
-
* const report = await ablo.weatherReports.retrieve('report_stockholm');
|
|
9
|
-
* await ablo.weatherReports.update(
|
|
8
|
+
* const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
|
|
9
|
+
* await ablo.weatherReports.update({
|
|
10
|
+
* id: 'report_stockholm',
|
|
11
|
+
* data: { status: 'ready' },
|
|
12
|
+
* });
|
|
10
13
|
*
|
|
11
14
|
* type Entry = Ablo.Peer;
|
|
12
15
|
* ```
|
|
@@ -23,7 +26,7 @@
|
|
|
23
26
|
* @abloatai/ablo/react — <AbloProvider>, useQuery, useMutate
|
|
24
27
|
* @abloatai/ablo/testing — test harnesses + mocks
|
|
25
28
|
*
|
|
26
|
-
* Reads split by where the data comes from. `ablo.<model>.retrieve(id)` and
|
|
29
|
+
* Reads split by where the data comes from. `ablo.<model>.retrieve({ id })` and
|
|
27
30
|
* `.list({ where })` are the async **server** reads (pool → IDB → network via
|
|
28
31
|
* the `HydrationCoordinator`, single-flight deduped); they're the default and
|
|
29
32
|
* what hosted/stateless callers want, since their local graph starts empty.
|
|
@@ -33,7 +36,7 @@
|
|
|
33
36
|
*
|
|
34
37
|
* ── What to import (read this first) ────────────────────────────────
|
|
35
38
|
* Default path — this is all most apps and agents ever need:
|
|
36
|
-
* • `Ablo` (default export) + `AbloOptions` + the `Model*
|
|
39
|
+
* • `Ablo` (default export) + `AbloOptions` + the `Model*Params` bags
|
|
37
40
|
* • the `Ablo*Error` classes, to discriminate failures in catch blocks
|
|
38
41
|
* That's it. If you're reaching past those, you're in advanced territory.
|
|
39
42
|
*
|
|
@@ -46,16 +49,22 @@
|
|
|
46
49
|
* If you don't recognize one, you don't need it — the default path covers you.
|
|
47
50
|
*/
|
|
48
51
|
export { Ablo } from './client/Ablo.js';
|
|
49
|
-
export type {
|
|
52
|
+
export type { MutationExecutor } from './interfaces/index.js';
|
|
53
|
+
export type { HttpClaimApi, InternalAbloOptions } from './client/Ablo.js';
|
|
54
|
+
export { createAbloHttpClient, type AbloHttpClientOptions, type AbloHttpClient, type HttpModelClient, } from './client/httpClient.js';
|
|
55
|
+
export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './client/auth.js';
|
|
56
|
+
export type { AbloOptions, ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, ClaimHandle, ModelOperations, } from './client/Ablo.js';
|
|
50
57
|
export type { AbloPersistence } from './client/persistence.js';
|
|
51
58
|
export { session, agent } from './principal.js';
|
|
52
59
|
import { Ablo } from './client/Ablo.js';
|
|
53
60
|
export default Ablo;
|
|
54
|
-
export { dataSource, abloSource, signAbloSourceRequest, verifyAbloSourceRequest, } from './source/index.js';
|
|
61
|
+
export { dataSource, abloSource, sourceEventForOperation, signAbloSourceRequest, verifyAbloSourceRequest, } from './source/index.js';
|
|
55
62
|
export { defaultPolicy, capabilityPreemptPolicy } from './policy/index.js';
|
|
56
|
-
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, } from './errors.js';
|
|
63
|
+
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, classifyRecovery, recoveryClassSchema, RECOVERY_CLASSES, } from './errors.js';
|
|
57
64
|
export type { CommitReceipt, RequiredCapability } from './errors.js';
|
|
58
|
-
export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec } from './errors.js';
|
|
65
|
+
export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec, RecoveryClass } from './errors.js';
|
|
66
|
+
export { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL } from './auth/credentialSource.js';
|
|
67
|
+
export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
|
|
59
68
|
export type { Register, DefaultSyncShape } from './types/global.js';
|
|
60
69
|
export { defineMutators } from './mutators/defineMutators.js';
|
|
61
70
|
export { createTransaction, type Transaction } from './mutators/Transaction.js';
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* import Ablo from '@abloatai/ablo';
|
|
6
6
|
*
|
|
7
7
|
* const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
8
|
-
* const report = await ablo.weatherReports.retrieve('report_stockholm');
|
|
9
|
-
* await ablo.weatherReports.update(
|
|
8
|
+
* const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
|
|
9
|
+
* await ablo.weatherReports.update({
|
|
10
|
+
* id: 'report_stockholm',
|
|
11
|
+
* data: { status: 'ready' },
|
|
12
|
+
* });
|
|
10
13
|
*
|
|
11
14
|
* type Entry = Ablo.Peer;
|
|
12
15
|
* ```
|
|
@@ -23,7 +26,7 @@
|
|
|
23
26
|
* @abloatai/ablo/react — <AbloProvider>, useQuery, useMutate
|
|
24
27
|
* @abloatai/ablo/testing — test harnesses + mocks
|
|
25
28
|
*
|
|
26
|
-
* Reads split by where the data comes from. `ablo.<model>.retrieve(id)` and
|
|
29
|
+
* Reads split by where the data comes from. `ablo.<model>.retrieve({ id })` and
|
|
27
30
|
* `.list({ where })` are the async **server** reads (pool → IDB → network via
|
|
28
31
|
* the `HydrationCoordinator`, single-flight deduped); they're the default and
|
|
29
32
|
* what hosted/stateless callers want, since their local graph starts empty.
|
|
@@ -33,7 +36,7 @@
|
|
|
33
36
|
*
|
|
34
37
|
* ── What to import (read this first) ────────────────────────────────
|
|
35
38
|
* Default path — this is all most apps and agents ever need:
|
|
36
|
-
* • `Ablo` (default export) + `AbloOptions` + the `Model*
|
|
39
|
+
* • `Ablo` (default export) + `AbloOptions` + the `Model*Params` bags
|
|
37
40
|
* • the `Ablo*Error` classes, to discriminate failures in catch blocks
|
|
38
41
|
* That's it. If you're reaching past those, you're in advanced territory.
|
|
39
42
|
*
|
|
@@ -53,6 +56,8 @@
|
|
|
53
56
|
// `import Ablo from '@abloatai/ablo'` works; named export so
|
|
54
57
|
// `import { Ablo }` also compiles.
|
|
55
58
|
export { Ablo } from './client/Ablo.js';
|
|
59
|
+
export { createAbloHttpClient, } from './client/httpClient.js';
|
|
60
|
+
export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './client/auth.js';
|
|
56
61
|
// Participant types live under `Ablo.Participant.*` —
|
|
57
62
|
// `Ablo.Participant.Joined`, `Ablo.Participant.Manager`,
|
|
58
63
|
// `Ablo.Participant.JoinOptions`, etc. Same dot-access shape as
|
|
@@ -70,7 +75,7 @@ export default Ablo;
|
|
|
70
75
|
// storage — if you haven't deliberately chosen to keep your own DB
|
|
71
76
|
// canonical, skip this entirely. Type counterparts live under
|
|
72
77
|
// `Ablo.Source.*` (`Ablo.Source.Operation`, `Ablo.Source.Commit.Params`).
|
|
73
|
-
export { dataSource, abloSource, signAbloSourceRequest, verifyAbloSourceRequest, } from './source/index.js';
|
|
78
|
+
export { dataSource, abloSource, sourceEventForOperation, signAbloSourceRequest, verifyAbloSourceRequest, } from './source/index.js';
|
|
74
79
|
// Schema DSL is intentionally published from `@abloatai/ablo/schema`.
|
|
75
80
|
// Keeping it out of the root import preserves one clean runtime surface:
|
|
76
81
|
// `import Ablo from '@abloatai/ablo'`.
|
|
@@ -82,7 +87,11 @@ export { defaultPolicy, capabilityPreemptPolicy } from './policy/index.js';
|
|
|
82
87
|
// Typed error hierarchy — Stripe-style. One import gets every class
|
|
83
88
|
// consumers need to discriminate failures (`e instanceof AbloX` or
|
|
84
89
|
// `e.type === 'AbloX'`) plus the HTTP-response translator.
|
|
85
|
-
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, } from './errors.js';
|
|
90
|
+
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, classifyRecovery, recoveryClassSchema, RECOVERY_CLASSES, } from './errors.js';
|
|
91
|
+
export { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL } from './auth/credentialSource.js';
|
|
92
|
+
// Storage-wedge detection — lets app shells render a recovery screen when the
|
|
93
|
+
// IndexedDB backing store is stuck (see core/openIDBWithTimeout.ts).
|
|
94
|
+
export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
|
|
86
95
|
// Advanced — most apps never import this. Custom (Zero-style) mutators:
|
|
87
96
|
// `ablo.<model>.create/update/delete` already covers normal writes. Reach
|
|
88
97
|
// for `defineMutators` only when you need a named, multi-step mutation with
|
package/dist/keys/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* still validate by hash — they parse here as `checksummed: false`.
|
|
17
17
|
*/
|
|
18
18
|
import { z } from 'zod';
|
|
19
|
-
export declare const API_KEY_KINDS: readonly ["secret", "restricted", "ephemeral"];
|
|
19
|
+
export declare const API_KEY_KINDS: readonly ["secret", "restricted", "ephemeral", "publishable"];
|
|
20
20
|
export type ApiKeyKind = (typeof API_KEY_KINDS)[number];
|
|
21
21
|
export declare const API_KEY_ENVS: readonly ["live", "test"];
|
|
22
22
|
export type ApiKeyEnv = (typeof API_KEY_ENVS)[number];
|
|
@@ -59,3 +59,18 @@ export declare function generateApiKey(env?: ApiKeyEnv, kind?: ApiKeyKind): {
|
|
|
59
59
|
* defend against. Used at both write (mint) and lookup.
|
|
60
60
|
*/
|
|
61
61
|
export declare function hashApiKey(plaintext: string): string;
|
|
62
|
+
/** `whsec_` label prefix per the Standard Webhooks spec (not part of the key material). */
|
|
63
|
+
export declare const WEBHOOK_SECRET_PREFIX = "whsec_";
|
|
64
|
+
/**
|
|
65
|
+
* Mint a webhook signing secret per the Standard Webhooks spec
|
|
66
|
+
* (https://www.standardwebhooks.com): a base64-encoded random key, 24–64 bytes,
|
|
67
|
+
* labelled with the `whsec_` prefix. We use 32 bytes (256 bits) — comfortably
|
|
68
|
+
* inside the range and matching Stripe/Svix. Unlike an API key this is NOT
|
|
69
|
+
* hashed at rest: signing (`signAbloSourceRequest`) needs the live key, so it is
|
|
70
|
+
* stored by reference via the secret store, returned to the customer once at
|
|
71
|
+
* creation, and never echoed again (Stripe's policy).
|
|
72
|
+
*/
|
|
73
|
+
export declare function generateWebhookSecret(): {
|
|
74
|
+
plaintext: string;
|
|
75
|
+
last4: string;
|
|
76
|
+
};
|
package/dist/keys/index.js
CHANGED
|
@@ -18,24 +18,29 @@
|
|
|
18
18
|
import { createHash, randomBytes } from 'node:crypto';
|
|
19
19
|
import { z } from 'zod';
|
|
20
20
|
// ── Vocabulary ──────────────────────────────────────────────────────────
|
|
21
|
-
// The
|
|
21
|
+
// The Stripe-style key model:
|
|
22
22
|
// secret (sk_) — backend / server-to-server / agents. Full authority. Never in a browser.
|
|
23
23
|
// restricted (rk_) — scoped SERVER key (agent session tokens / capabilities).
|
|
24
24
|
// ephemeral (ek_) — short-lived, backend-minted, USER-scoped BROWSER session credential
|
|
25
25
|
// (Stripe ephemeral keys). Carries participantKind:'user' + baked syncGroups.
|
|
26
|
-
// (
|
|
27
|
-
//
|
|
28
|
-
|
|
26
|
+
// publishable (pk_) — long-lived, browser-safe, org-scoped, READ-ONLY project key
|
|
27
|
+
// (Stripe `pk_` / Supabase anon key). Used DIRECTLY as the bearer
|
|
28
|
+
// (never exchanged, never expires → nothing to refresh). The org owns
|
|
29
|
+
// it; it grants read access to the org's data plane and cannot write
|
|
30
|
+
// or reach any control-plane operation.
|
|
31
|
+
export const API_KEY_KINDS = ['secret', 'restricted', 'ephemeral', 'publishable'];
|
|
29
32
|
export const API_KEY_ENVS = ['live', 'test'];
|
|
30
33
|
const PREFIX_BY_KIND = {
|
|
31
34
|
secret: 'sk',
|
|
32
35
|
restricted: 'rk',
|
|
33
36
|
ephemeral: 'ek',
|
|
37
|
+
publishable: 'pk',
|
|
34
38
|
};
|
|
35
39
|
const KIND_BY_PREFIX = {
|
|
36
40
|
sk: 'secret',
|
|
37
41
|
rk: 'restricted',
|
|
38
42
|
ek: 'ephemeral',
|
|
43
|
+
pk: 'publishable',
|
|
39
44
|
};
|
|
40
45
|
const BASE62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
41
46
|
/** Random base62 chars before the checksum. */
|
|
@@ -44,8 +49,8 @@ const KEY_BODY_LEN = 30;
|
|
|
44
49
|
const CHECKSUM_LEN = 6;
|
|
45
50
|
/** A new checksummed body is exactly this long and pure base62. */
|
|
46
51
|
const CHECKSUMMED_BODY_LEN = KEY_BODY_LEN + CHECKSUM_LEN;
|
|
47
|
-
/** `<sk|rk|ek>_<live|test>_<body>`; body charset covers base62 AND legacy base64url. */
|
|
48
|
-
const KEY_RE = /^(sk|rk|ek)_(live|test)_([0-9A-Za-z\-_]+)$/;
|
|
52
|
+
/** `<sk|rk|ek|pk>_<live|test>_<body>`; body charset covers base62 AND legacy base64url. */
|
|
53
|
+
const KEY_RE = /^(sk|rk|ek|pk)_(live|test)_([0-9A-Za-z\-_]+)$/;
|
|
49
54
|
const BASE62_RE = /^[0-9A-Za-z]+$/;
|
|
50
55
|
// ── Checksum (standard CRC-32, GitHub-compatible) ───────────────────────
|
|
51
56
|
const CRC32_TABLE = (() => {
|
|
@@ -149,3 +154,18 @@ export function generateApiKey(env = 'live', kind = 'secret') {
|
|
|
149
154
|
export function hashApiKey(plaintext) {
|
|
150
155
|
return createHash('sha256').update(plaintext).digest('hex');
|
|
151
156
|
}
|
|
157
|
+
/** `whsec_` label prefix per the Standard Webhooks spec (not part of the key material). */
|
|
158
|
+
export const WEBHOOK_SECRET_PREFIX = 'whsec_';
|
|
159
|
+
/**
|
|
160
|
+
* Mint a webhook signing secret per the Standard Webhooks spec
|
|
161
|
+
* (https://www.standardwebhooks.com): a base64-encoded random key, 24–64 bytes,
|
|
162
|
+
* labelled with the `whsec_` prefix. We use 32 bytes (256 bits) — comfortably
|
|
163
|
+
* inside the range and matching Stripe/Svix. Unlike an API key this is NOT
|
|
164
|
+
* hashed at rest: signing (`signAbloSourceRequest`) needs the live key, so it is
|
|
165
|
+
* stored by reference via the secret store, returned to the customer once at
|
|
166
|
+
* creation, and never echoed again (Stripe's policy).
|
|
167
|
+
*/
|
|
168
|
+
export function generateWebhookSecret() {
|
|
169
|
+
const plaintext = `${WEBHOOK_SECRET_PREFIX}${randomBytes(32).toString('base64')}`;
|
|
170
|
+
return { plaintext, last4: plaintext.slice(-4) };
|
|
171
|
+
}
|
|
@@ -19,56 +19,21 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { Schema } from '../schema/schema.js';
|
|
21
21
|
import type { SyncStoreContract } from '../react/context.js';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* is schema-agnostic — the transaction it replays through is schema-typed.
|
|
27
|
-
*/
|
|
28
|
-
export type InverseOp = {
|
|
29
|
-
kind: 'create';
|
|
30
|
-
modelKey: string;
|
|
31
|
-
data: Record<string, unknown>;
|
|
32
|
-
} | {
|
|
33
|
-
kind: 'update';
|
|
34
|
-
modelKey: string;
|
|
35
|
-
patch: {
|
|
36
|
-
id: string;
|
|
37
|
-
} & Record<string, unknown>;
|
|
38
|
-
} | {
|
|
39
|
-
kind: 'delete';
|
|
40
|
-
modelKey: string;
|
|
41
|
-
id: string;
|
|
42
|
-
} | {
|
|
43
|
-
kind: 'createMany';
|
|
44
|
-
modelKey: string;
|
|
45
|
-
data: Record<string, unknown>[];
|
|
46
|
-
} | {
|
|
47
|
-
kind: 'updateMany';
|
|
48
|
-
modelKey: string;
|
|
49
|
-
patches: Array<{
|
|
50
|
-
id: string;
|
|
51
|
-
} & Record<string, unknown>>;
|
|
52
|
-
} | {
|
|
53
|
-
kind: 'deleteMany';
|
|
54
|
-
modelKey: string;
|
|
55
|
-
ids: string[];
|
|
56
|
-
};
|
|
57
|
-
/** One undo entry = one mutator invocation's set of inverses, in reverse order. */
|
|
58
|
-
export interface UndoEntry {
|
|
59
|
-
/** Optional label for diagnostics / UI ("Move layer", "Delete slide", etc). */
|
|
60
|
-
label?: string;
|
|
61
|
-
inverses: InverseOp[];
|
|
62
|
-
/**
|
|
63
|
-
* Paired forward ops, captured at record time so redo can replay them
|
|
64
|
-
* without re-running the user's mutator (which may have non-idempotent
|
|
65
|
-
* side effects like generating new IDs).
|
|
66
|
-
*/
|
|
67
|
-
forwards: InverseOp[];
|
|
68
|
-
}
|
|
22
|
+
import { type InverseOp, type UndoEntry } from './inverseOp.js';
|
|
23
|
+
import { type UndoConflictPolicy } from './undoApply.js';
|
|
24
|
+
export type { InverseOp, UndoEntry };
|
|
25
|
+
export type { UndoConflictPolicy } from './undoApply.js';
|
|
69
26
|
export interface UndoScopeOptions {
|
|
70
27
|
/** Max number of undo entries. Older entries drop off the bottom. Default: 100. */
|
|
71
28
|
maxHistory?: number;
|
|
29
|
+
/**
|
|
30
|
+
* How undo/redo treats a field a collaborator changed after your op.
|
|
31
|
+
* Default `skip-stale` — your undo reverts your change only where it still
|
|
32
|
+
* stands, never clobbering a concurrent collaborator edit (per-user undo).
|
|
33
|
+
* `last-writer-wins` restores the legacy clobbering behavior. See
|
|
34
|
+
* {@link UndoConflictPolicy}.
|
|
35
|
+
*/
|
|
36
|
+
conflictPolicy?: UndoConflictPolicy;
|
|
72
37
|
}
|
|
73
38
|
/**
|
|
74
39
|
* A single undo stack for one surface. Access via `UndoManager.getScope(name)`.
|
|
@@ -82,14 +47,85 @@ export declare class UndoScope<S extends Schema> {
|
|
|
82
47
|
private undoStack;
|
|
83
48
|
private redoStack;
|
|
84
49
|
private readonly maxHistory;
|
|
50
|
+
private readonly conflictPolicy;
|
|
51
|
+
/**
|
|
52
|
+
* Observers notified after each successful {@link record}. These see FORWARD
|
|
53
|
+
* user actions only — `undo()`/`redo()` replays move entries between stacks
|
|
54
|
+
* without calling `record()`, so a listener never observes a reversal. This
|
|
55
|
+
* is a deliberately domain-agnostic seam: analytics, gamification, and audit
|
|
56
|
+
* can tap the committed-mutation stream without the scope knowing about them.
|
|
57
|
+
* A throwing listener is isolated (see {@link emitRecord}) so a faulty
|
|
58
|
+
* observer can never wedge the editor's recording path.
|
|
59
|
+
*/
|
|
60
|
+
private readonly recordListeners;
|
|
61
|
+
/**
|
|
62
|
+
* Serialization tail. Recording, undo, and redo all chain off this single
|
|
63
|
+
* promise so they run strictly in the order they were *invoked* — never
|
|
64
|
+
* interleaved. This is load-bearing for correctness, not just throughput:
|
|
65
|
+
* - Ordering: callers fire writes un-awaited (`void mutations.x.update`).
|
|
66
|
+
* Without serialization, an entry lands on the stack when its mutator
|
|
67
|
+
* *resolves*, so a fast second write can record before a slow first one
|
|
68
|
+
* → undo replays in the wrong order.
|
|
69
|
+
* - Snapshot integrity: every recording reads/clears the shared models'
|
|
70
|
+
* `modifiedProperties` (the undo "before" baseline). Two recordings
|
|
71
|
+
* interleaving on the same model corrupt each other's inverse snapshot.
|
|
72
|
+
* Serializing the whole scope closes both holes with one mechanism.
|
|
73
|
+
*/
|
|
74
|
+
private tail;
|
|
85
75
|
constructor(schema: S, store: SyncStoreContract, organizationId: string, options?: UndoScopeOptions);
|
|
86
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Run `work` after every previously-enqueued scope operation has settled,
|
|
78
|
+
* in invocation order. The internal `tail` always resolves (failures are
|
|
79
|
+
* swallowed *for the chain only*) so one rejected mutator can't wedge the
|
|
80
|
+
* queue; the original settlement is still surfaced to this call's caller.
|
|
81
|
+
*/
|
|
82
|
+
private enqueue;
|
|
83
|
+
/**
|
|
84
|
+
* Run a recording mutator exclusively on the scope's serialization chain.
|
|
85
|
+
* `useMutators` calls this so the snapshot → write → `record()` sequence is
|
|
86
|
+
* atomic relative to other invocations, undo, and redo.
|
|
87
|
+
*/
|
|
88
|
+
runRecorded<T>(work: () => Promise<T>): Promise<T>;
|
|
89
|
+
/**
|
|
90
|
+
* Internal: record a mutator's inverses. Clears the redo stack.
|
|
91
|
+
*
|
|
92
|
+
* Entries here are produced internally by `RecordingTransaction` (trusted),
|
|
93
|
+
* so the schema check is DEV-ONLY: it catches recorder bugs in dev/test
|
|
94
|
+
* (rejecting a malformed op at ingestion, with its path, instead of letting
|
|
95
|
+
* it crash later inside `applyOps`) without paying a Zod parse on every user
|
|
96
|
+
* action in production. The real validation boundary is `parseUndoEntry`,
|
|
97
|
+
* applied when entries are deserialized from persistence (untrusted input).
|
|
98
|
+
* Best practice: validate at trust boundaries, type-check internal calls.
|
|
99
|
+
*/
|
|
87
100
|
record(entry: UndoEntry): void;
|
|
101
|
+
/**
|
|
102
|
+
* Subscribe to every recorded mutation. Fires synchronously at the tail of
|
|
103
|
+
* each {@link record} call, after the entry is on the undo stack. Returns an
|
|
104
|
+
* unsubscribe function — call it on teardown.
|
|
105
|
+
*
|
|
106
|
+
* Listeners receive the full {@link UndoEntry} (its `forwards` carry the
|
|
107
|
+
* `{ kind, modelKey, data }` ops), so a consumer can derive what changed
|
|
108
|
+
* (e.g. "a slideLayers row of type 'chart' was created") without re-querying.
|
|
109
|
+
*/
|
|
110
|
+
onRecord(listener: (entry: UndoEntry) => void): () => void;
|
|
111
|
+
private emitRecord;
|
|
88
112
|
canUndo(): boolean;
|
|
89
113
|
canRedo(): boolean;
|
|
90
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Pop the last mutator and apply its inverses. Pushes to redo.
|
|
116
|
+
*
|
|
117
|
+
* Under the default `skip-stale` policy the inverses are filtered against
|
|
118
|
+
* live state first (paired with the entry's forwards = "what I set"), so a
|
|
119
|
+
* field a collaborator changed after my op is left untouched — undo reverts
|
|
120
|
+
* my change only where it still stands.
|
|
121
|
+
*/
|
|
91
122
|
undo(): Promise<void>;
|
|
92
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* Pop the last undone entry and re-apply the forward ops. Pushes to undo.
|
|
125
|
+
* Symmetric to {@link undo}: forwards are filtered against live state
|
|
126
|
+
* (paired with the entry's inverses = "what undo restored"), so redo
|
|
127
|
+
* re-asserts my change only where the undone value still stands.
|
|
128
|
+
*/
|
|
93
129
|
redo(): Promise<void>;
|
|
94
130
|
/** Drop all history. Use after bootstrap / sync group change / sync error. */
|
|
95
131
|
clear(): void;
|