@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.
Files changed (162) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +32 -27
  3. package/dist/BaseSyncedStore.d.ts +73 -0
  4. package/dist/BaseSyncedStore.js +172 -2
  5. package/dist/Model.d.ts +42 -0
  6. package/dist/Model.js +103 -44
  7. package/dist/agent/session.js +3 -3
  8. package/dist/ai-sdk/coordination-context.js +4 -0
  9. package/dist/ai-sdk/index.d.ts +56 -47
  10. package/dist/ai-sdk/index.js +56 -47
  11. package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
  12. package/dist/ai-sdk/intent-broadcast.js +11 -4
  13. package/dist/ai-sdk/wrap.d.ts +14 -11
  14. package/dist/ai-sdk/wrap.js +11 -13
  15. package/dist/auth/credentialSource.d.ts +34 -0
  16. package/dist/auth/credentialSource.js +63 -0
  17. package/dist/auth/index.d.ts +2 -22
  18. package/dist/auth/index.js +4 -42
  19. package/dist/auth/schemas.d.ts +35 -0
  20. package/dist/auth/schemas.js +53 -0
  21. package/dist/client/Ablo.d.ts +160 -42
  22. package/dist/client/Ablo.js +145 -75
  23. package/dist/client/ApiClient.d.ts +20 -4
  24. package/dist/client/ApiClient.js +166 -28
  25. package/dist/client/auth.d.ts +14 -5
  26. package/dist/client/auth.js +60 -7
  27. package/dist/client/createInternalComponents.d.ts +2 -0
  28. package/dist/client/createInternalComponents.js +8 -1
  29. package/dist/client/createModelProxy.d.ts +130 -66
  30. package/dist/client/createModelProxy.js +152 -49
  31. package/dist/client/httpClient.d.ts +71 -0
  32. package/dist/client/httpClient.js +69 -0
  33. package/dist/client/identity.d.ts +2 -6
  34. package/dist/client/identity.js +49 -11
  35. package/dist/client/index.d.ts +1 -0
  36. package/dist/client/index.js +1 -0
  37. package/dist/client/registerDataSource.d.ts +3 -3
  38. package/dist/client/registerDataSource.js +11 -9
  39. package/dist/client/validateAbloOptions.js +1 -1
  40. package/dist/core/DatabaseManager.js +30 -2
  41. package/dist/core/openIDBWithTimeout.d.ts +36 -0
  42. package/dist/core/openIDBWithTimeout.js +88 -1
  43. package/dist/errorCodes.d.ts +70 -1
  44. package/dist/errorCodes.js +108 -9
  45. package/dist/errors.d.ts +2 -2
  46. package/dist/errors.js +72 -22
  47. package/dist/index.d.ts +17 -8
  48. package/dist/index.js +15 -6
  49. package/dist/keys/index.d.ts +16 -1
  50. package/dist/keys/index.js +26 -6
  51. package/dist/mutators/UndoManager.d.ts +86 -50
  52. package/dist/mutators/UndoManager.js +129 -22
  53. package/dist/mutators/inverseOp.d.ts +129 -0
  54. package/dist/mutators/inverseOp.js +74 -0
  55. package/dist/mutators/readerActions.d.ts +1 -1
  56. package/dist/mutators/undoApply.d.ts +42 -0
  57. package/dist/mutators/undoApply.js +143 -0
  58. package/dist/query/client.d.ts +10 -9
  59. package/dist/query/client.js +3 -6
  60. package/dist/react/AbloProvider.d.ts +23 -126
  61. package/dist/react/AbloProvider.js +62 -199
  62. package/dist/react/useAblo.d.ts +2 -2
  63. package/dist/react/useCurrentUserId.d.ts +1 -1
  64. package/dist/react/useCurrentUserId.js +1 -1
  65. package/dist/react/useMutators.js +19 -12
  66. package/dist/schema/ddl.d.ts +26 -3
  67. package/dist/schema/ddl.js +152 -4
  68. package/dist/schema/index.d.ts +4 -0
  69. package/dist/schema/index.js +12 -0
  70. package/dist/schema/model.d.ts +11 -0
  71. package/dist/schema/model.js +2 -0
  72. package/dist/schema/openapi.d.ts +28 -0
  73. package/dist/schema/openapi.js +118 -0
  74. package/dist/schema/plane.d.ts +23 -0
  75. package/dist/schema/plane.js +19 -0
  76. package/dist/schema/relation.d.ts +20 -0
  77. package/dist/schema/serialize.d.ts +4 -0
  78. package/dist/schema/serialize.js +4 -0
  79. package/dist/schema/sync-delta-row.d.ts +157 -0
  80. package/dist/schema/sync-delta-row.js +102 -0
  81. package/dist/schema/sync-delta-wire.d.ts +180 -0
  82. package/dist/schema/sync-delta-wire.js +102 -0
  83. package/dist/server/adapter.d.ts +156 -0
  84. package/dist/server/adapter.js +19 -0
  85. package/dist/server/commit.d.ts +82 -0
  86. package/dist/server/commit.js +1 -0
  87. package/dist/server/index.d.ts +14 -0
  88. package/dist/server/index.js +1 -0
  89. package/dist/server/next.d.ts +51 -0
  90. package/dist/server/next.js +47 -0
  91. package/dist/server/read-config.d.ts +60 -0
  92. package/dist/server/read-config.js +8 -0
  93. package/dist/server/storage-mode.d.ts +17 -0
  94. package/dist/server/storage-mode.js +12 -0
  95. package/dist/source/adapter.d.ts +59 -0
  96. package/dist/source/adapter.js +19 -0
  97. package/dist/source/adapters/drizzle.d.ts +34 -0
  98. package/dist/source/adapters/drizzle.js +147 -0
  99. package/dist/source/adapters/memory.d.ts +12 -0
  100. package/dist/source/adapters/memory.js +114 -0
  101. package/dist/source/adapters/prisma.d.ts +57 -0
  102. package/dist/source/adapters/prisma.js +199 -0
  103. package/dist/source/conformance.d.ts +32 -0
  104. package/dist/source/conformance.js +134 -0
  105. package/dist/source/contract.d.ts +143 -0
  106. package/dist/source/contract.js +98 -0
  107. package/dist/source/index.d.ts +61 -10
  108. package/dist/source/index.js +98 -0
  109. package/dist/source/next.d.ts +33 -0
  110. package/dist/source/next.js +26 -0
  111. package/dist/sync/BootstrapHelper.d.ts +10 -0
  112. package/dist/sync/BootstrapHelper.js +10 -15
  113. package/dist/sync/ConnectionManager.d.ts +55 -1
  114. package/dist/sync/ConnectionManager.js +155 -16
  115. package/dist/sync/HydrationCoordinator.d.ts +93 -17
  116. package/dist/sync/HydrationCoordinator.js +238 -39
  117. package/dist/sync/NetworkProbe.d.ts +58 -24
  118. package/dist/sync/NetworkProbe.js +118 -42
  119. package/dist/sync/SyncWebSocket.d.ts +45 -70
  120. package/dist/sync/SyncWebSocket.js +70 -36
  121. package/dist/sync/createIntentStream.js +10 -1
  122. package/dist/types/streams.d.ts +9 -0
  123. package/dist/utils/mobx-setup.js +1 -0
  124. package/dist/webhooks/events.d.ts +38 -0
  125. package/dist/webhooks/events.js +40 -0
  126. package/dist/webhooks/index.d.ts +10 -0
  127. package/dist/webhooks/index.js +10 -0
  128. package/dist/wire/errorEnvelope.d.ts +34 -0
  129. package/dist/wire/errorEnvelope.js +86 -0
  130. package/dist/wire/frames.d.ts +119 -0
  131. package/dist/wire/frames.js +1 -0
  132. package/dist/wire/index.d.ts +24 -0
  133. package/dist/wire/index.js +21 -0
  134. package/dist/wire/listEnvelope.d.ts +45 -0
  135. package/dist/wire/listEnvelope.js +17 -0
  136. package/docs/api.md +47 -44
  137. package/docs/cli.md +44 -44
  138. package/docs/client-behavior.md +30 -30
  139. package/docs/coordination.md +33 -36
  140. package/docs/data-sources.md +35 -15
  141. package/docs/examples/agent-human.md +45 -43
  142. package/docs/examples/ai-sdk-tool.md +20 -16
  143. package/docs/examples/existing-python-backend.md +16 -12
  144. package/docs/examples/nextjs.md +14 -12
  145. package/docs/examples/scoped-agent.md +1 -1
  146. package/docs/examples/server-agent.md +24 -21
  147. package/docs/guarantees.md +15 -13
  148. package/docs/index.md +1 -1
  149. package/docs/integration-guide.md +30 -30
  150. package/docs/interaction-model.md +19 -23
  151. package/docs/mcp/claude-code.md +3 -3
  152. package/docs/mcp/cursor.md +1 -1
  153. package/docs/mcp/windsurf.md +2 -2
  154. package/docs/mcp.md +6 -6
  155. package/docs/quickstart.md +41 -31
  156. package/docs/react.md +13 -9
  157. package/docs/schema-contract.md +12 -10
  158. package/docs/the-loop.md +21 -0
  159. package/examples/data-source/README.md +4 -5
  160. package/examples/data-source/customer-server.ts +27 -25
  161. package/llms.txt +28 -5
  162. 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 { errorCodeSpec } from './errorCodes.js';
22
- export { ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode } from './errorCodes.js';
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
- // Prefer the structured error code: ONLY a genuine session / JWT EXPIRY
242
- // should drive sign-out + re-auth. A non-expiry auth failure
243
- // (api_key_required, jwt_issuer_untrusted, a 403 permission denial, …) must
244
- // NOT — re-authenticating re-mints the same credential and loops, which is
245
- // exactly the "flash then bounce to /signin" symptom. This mirrors the
246
- // canonical wire mapping (401 → Authentication, 403 → Permission).
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 === 'session_expired' || code === 'jwt_expired';
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 = typeof body === 'object' && body !== null ? body : {};
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
- if (typeof body !== 'object' || body === null)
367
- return false;
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 b.error === 'string')
421
+ if (typeof parsed.error === 'string')
372
422
  return true;
373
- return (typeof b.error === 'object' &&
374
- b.error !== null &&
375
- typeof b.error.code === 'string');
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 === 'object' &&
399
- b.error !== null &&
400
- typeof b.error.code === 'string') {
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('report_stockholm', { status: 'ready' });
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*Options` bags
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 { AbloOptions, ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ClaimOptions, ClaimedRow, ModelOperations, } from './client/Ablo.js';
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('report_stockholm', { status: 'ready' });
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*Options` bags
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
@@ -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
+ };
@@ -18,24 +18,29 @@
18
18
  import { createHash, randomBytes } from 'node:crypto';
19
19
  import { z } from 'zod';
20
20
  // ── Vocabulary ──────────────────────────────────────────────────────────
21
- // The three-key Stripe model:
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
- // (There is no publishable `pk_`the minted session token, not a project
27
- // identifier, is what the browser holds; it already names the org.)
28
- export const API_KEY_KINDS = ['secret', 'restricted', 'ephemeral'];
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
- * A single reversible operation. The runtime captures these during a
24
- * recorded transaction and replays them (in reverse order) on undo.
25
- * Model keys and data shapes are stored as strings/records so the manager
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
- /** Internal: record a mutator's inverses. Clears the redo stack. */
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
- /** Pop the last mutator and apply its inverses. Pushes to redo. */
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
- /** Pop the last undone entry and re-apply the forward ops. Pushes to undo. */
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;