@abloatai/ablo 0.8.0 → 0.9.1

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 (165) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/README.md +33 -28
  3. package/dist/BaseSyncedStore.d.ts +83 -0
  4. package/dist/BaseSyncedStore.js +194 -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 +158 -50
  52. package/dist/mutators/UndoManager.js +345 -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/context.d.ts +31 -0
  63. package/dist/react/useAblo.d.ts +2 -2
  64. package/dist/react/useCurrentUserId.d.ts +1 -1
  65. package/dist/react/useCurrentUserId.js +1 -1
  66. package/dist/react/useMutators.js +19 -12
  67. package/dist/schema/ddl.d.ts +34 -3
  68. package/dist/schema/ddl.js +162 -4
  69. package/dist/schema/index.d.ts +5 -1
  70. package/dist/schema/index.js +13 -1
  71. package/dist/schema/model.d.ts +11 -0
  72. package/dist/schema/model.js +2 -0
  73. package/dist/schema/openapi.d.ts +28 -0
  74. package/dist/schema/openapi.js +118 -0
  75. package/dist/schema/plane.d.ts +23 -0
  76. package/dist/schema/plane.js +19 -0
  77. package/dist/schema/relation.d.ts +20 -0
  78. package/dist/schema/serialize.d.ts +4 -0
  79. package/dist/schema/serialize.js +4 -0
  80. package/dist/schema/sync-delta-row.d.ts +157 -0
  81. package/dist/schema/sync-delta-row.js +102 -0
  82. package/dist/schema/sync-delta-wire.d.ts +180 -0
  83. package/dist/schema/sync-delta-wire.js +102 -0
  84. package/dist/server/adapter.d.ts +156 -0
  85. package/dist/server/adapter.js +19 -0
  86. package/dist/server/commit.d.ts +82 -0
  87. package/dist/server/commit.js +1 -0
  88. package/dist/server/index.d.ts +14 -0
  89. package/dist/server/index.js +1 -0
  90. package/dist/server/next.d.ts +51 -0
  91. package/dist/server/next.js +47 -0
  92. package/dist/server/read-config.d.ts +60 -0
  93. package/dist/server/read-config.js +8 -0
  94. package/dist/server/storage-mode.d.ts +17 -0
  95. package/dist/server/storage-mode.js +12 -0
  96. package/dist/source/adapter.d.ts +65 -0
  97. package/dist/source/adapter.js +20 -0
  98. package/dist/source/adapters/drizzle.d.ts +43 -0
  99. package/dist/source/adapters/drizzle.js +185 -0
  100. package/dist/source/adapters/memory.d.ts +12 -0
  101. package/dist/source/adapters/memory.js +114 -0
  102. package/dist/source/adapters/prisma.d.ts +57 -0
  103. package/dist/source/adapters/prisma.js +176 -0
  104. package/dist/source/conformance.d.ts +32 -0
  105. package/dist/source/conformance.js +134 -0
  106. package/dist/source/contract.d.ts +144 -0
  107. package/dist/source/contract.js +99 -0
  108. package/dist/source/index.d.ts +62 -10
  109. package/dist/source/index.js +99 -0
  110. package/dist/source/migrations.d.ts +14 -0
  111. package/dist/source/migrations.js +39 -0
  112. package/dist/source/next.d.ts +33 -0
  113. package/dist/source/next.js +26 -0
  114. package/dist/sync/BootstrapHelper.d.ts +10 -0
  115. package/dist/sync/BootstrapHelper.js +10 -15
  116. package/dist/sync/ConnectionManager.d.ts +55 -1
  117. package/dist/sync/ConnectionManager.js +155 -16
  118. package/dist/sync/HydrationCoordinator.d.ts +93 -17
  119. package/dist/sync/HydrationCoordinator.js +238 -39
  120. package/dist/sync/NetworkProbe.d.ts +58 -24
  121. package/dist/sync/NetworkProbe.js +118 -42
  122. package/dist/sync/SyncWebSocket.d.ts +45 -70
  123. package/dist/sync/SyncWebSocket.js +70 -36
  124. package/dist/sync/createIntentStream.js +10 -1
  125. package/dist/types/streams.d.ts +9 -0
  126. package/dist/utils/mobx-setup.js +1 -0
  127. package/dist/webhooks/events.d.ts +38 -0
  128. package/dist/webhooks/events.js +40 -0
  129. package/dist/webhooks/index.d.ts +10 -0
  130. package/dist/webhooks/index.js +10 -0
  131. package/dist/wire/errorEnvelope.d.ts +34 -0
  132. package/dist/wire/errorEnvelope.js +86 -0
  133. package/dist/wire/frames.d.ts +119 -0
  134. package/dist/wire/frames.js +1 -0
  135. package/dist/wire/index.d.ts +24 -0
  136. package/dist/wire/index.js +21 -0
  137. package/dist/wire/listEnvelope.d.ts +45 -0
  138. package/dist/wire/listEnvelope.js +17 -0
  139. package/docs/api.md +47 -44
  140. package/docs/cli.md +44 -44
  141. package/docs/client-behavior.md +30 -30
  142. package/docs/coordination.md +33 -36
  143. package/docs/data-sources.md +35 -15
  144. package/docs/examples/agent-human.md +45 -43
  145. package/docs/examples/ai-sdk-tool.md +20 -16
  146. package/docs/examples/existing-python-backend.md +16 -12
  147. package/docs/examples/nextjs.md +14 -12
  148. package/docs/examples/scoped-agent.md +1 -1
  149. package/docs/examples/server-agent.md +24 -21
  150. package/docs/guarantees.md +15 -13
  151. package/docs/index.md +2 -2
  152. package/docs/integration-guide.md +30 -30
  153. package/docs/interaction-model.md +19 -23
  154. package/docs/mcp/claude-code.md +3 -3
  155. package/docs/mcp/cursor.md +1 -1
  156. package/docs/mcp/windsurf.md +2 -2
  157. package/docs/mcp.md +6 -6
  158. package/docs/quickstart.md +41 -31
  159. package/docs/react.md +13 -9
  160. package/docs/schema-contract.md +12 -10
  161. package/docs/the-loop.md +21 -0
  162. package/examples/data-source/README.md +4 -5
  163. package/examples/data-source/customer-server.ts +27 -25
  164. package/llms.txt +28 -5
  165. 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,38 @@
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;
37
+ /**
38
+ * Which models this surface owns. The scope only records mutations whose
39
+ * resolved schema key passes this predicate, so a spreadsheet edit never
40
+ * lands on the deck editor's stack (the equivalent of Yjs scoping by
41
+ * shared-type set). Omit to track every model — fine for a single-surface
42
+ * app, wrong when two surfaces with independent Cmd+Z share one store.
43
+ */
44
+ tracksModel?: (schemaKey: string) => boolean;
45
+ /**
46
+ * Opt into recording undo entries by OBSERVING the local-mutation stream
47
+ * (the best-practice model: undo listens where all local writes converge —
48
+ * Yjs/Liveblocks). When false (default), the scope records nothing on its
49
+ * own and relies on legacy manual `record()` calls. Transitional: a scope
50
+ * must not mix the two, or shared writes double-count. Flip a surface to
51
+ * `true` only when its manual-record consumers are removed in the same step.
52
+ */
53
+ recordFromStream?: boolean;
72
54
  }
73
55
  /**
74
56
  * A single undo stack for one surface. Access via `UndoManager.getScope(name)`.
@@ -82,14 +64,134 @@ export declare class UndoScope<S extends Schema> {
82
64
  private undoStack;
83
65
  private redoStack;
84
66
  private readonly maxHistory;
67
+ private readonly conflictPolicy;
68
+ /**
69
+ * Observers notified after each successful {@link record}. These see FORWARD
70
+ * user actions only — `undo()`/`redo()` replays move entries between stacks
71
+ * without calling `record()`, so a listener never observes a reversal. This
72
+ * is a deliberately domain-agnostic seam: analytics, gamification, and audit
73
+ * can tap the committed-mutation stream without the scope knowing about them.
74
+ * A throwing listener is isolated (see {@link emitRecord}) so a faulty
75
+ * observer can never wedge the editor's recording path.
76
+ */
77
+ private readonly recordListeners;
78
+ /**
79
+ * Serialization tail. Recording, undo, and redo all chain off this single
80
+ * promise so they run strictly in the order they were *invoked* — never
81
+ * interleaved. This is load-bearing for correctness, not just throughput:
82
+ * - Ordering: callers fire writes un-awaited (`void mutations.x.update`).
83
+ * Without serialization, an entry lands on the stack when its mutator
84
+ * *resolves*, so a fast second write can record before a slow first one
85
+ * → undo replays in the wrong order.
86
+ * - Snapshot integrity: every recording reads/clears the shared models'
87
+ * `modifiedProperties` (the undo "before" baseline). Two recordings
88
+ * interleaving on the same model corrupt each other's inverse snapshot.
89
+ * Serializing the whole scope closes both holes with one mechanism.
90
+ */
91
+ private tail;
92
+ /** Predicate selecting which models this surface records (see options). */
93
+ private readonly tracksModel?;
94
+ /** registered-name / alias → schema key, built once from the schema. */
95
+ private readonly schemaKeyByAlias;
96
+ /** Unsubscribe from the local-mutation stream. */
97
+ private readonly unsubscribe;
98
+ /**
99
+ * True while `undo()`/`redo()` replays ops. Replays write through the same
100
+ * commit path, so they re-emit on the local-mutation stream; this flag tells
101
+ * our own listener to ignore them (no echo) — the engine equivalent of Yjs's
102
+ * `trackedOrigins` exclusion / Liveblocks pausing history during undo.
103
+ */
104
+ private replaying;
105
+ /** Ops collected during the current tick, flushed as ONE entry. */
106
+ private batch;
107
+ private flushScheduled;
108
+ /**
109
+ * Open grouping session (Liveblocks `history.pause()` / Yjs `stopCapturing`
110
+ * analogue). While set, stream ops accumulate here ACROSS ticks instead of
111
+ * flushing per-tick, so a multi-tick action (a drag, a whole streaming AI
112
+ * response) collapses into ONE Cmd+Z. `endGroup()` flushes it.
113
+ */
114
+ private group;
85
115
  constructor(schema: S, store: SyncStoreContract, organizationId: string, options?: UndoScopeOptions);
86
- /** Internal: record a mutator's inverses. Clears the redo stack. */
116
+ /**
117
+ * Open a grouping session: every stream-recorded op until {@link endGroup}
118
+ * collapses into a single undo entry. Mirrors Liveblocks `history.pause()` —
119
+ * call on gesture start (pointerdown) or AI-response start. Idempotent-ish:
120
+ * a second call closes the previous group first.
121
+ */
122
+ beginGroup(label?: string): void;
123
+ /** Close the grouping session and record the accumulated ops as one entry. */
124
+ endGroup(label?: string): void;
125
+ /** Resolve a stream mutation's registered name to its schema key, or null. */
126
+ private resolveSchemaKey;
127
+ /**
128
+ * Stream listener — the sole place entries are born. Skips replay echoes
129
+ * and out-of-scope models, derives the forward+inverse op from the
130
+ * mutation's `data`/`previousData`, and defers the stack push to a
131
+ * per-tick flush so a burst of writes (e.g. align 5 layers) becomes ONE
132
+ * undo step — riding the same tick boundary the TransactionQueue batches on.
133
+ */
134
+ private onLocalMutation;
135
+ private scheduleFlush;
136
+ /** Coalesce the tick's collected ops into one entry and record it. */
137
+ private flushBatch;
138
+ /**
139
+ * Run `work` after every previously-enqueued scope operation has settled,
140
+ * in invocation order. The internal `tail` always resolves (failures are
141
+ * swallowed *for the chain only*) so one rejected mutator can't wedge the
142
+ * queue; the original settlement is still surfaced to this call's caller.
143
+ */
144
+ private enqueue;
145
+ /**
146
+ * Run a recording mutator exclusively on the scope's serialization chain.
147
+ * Used by the legacy manual-record path (`useMutators` + `RecordingTransaction`)
148
+ * so the snapshot → write → `record()` sequence is atomic relative to undo/
149
+ * redo. The stream-recording path doesn't need this (it derives entries from
150
+ * already-committed mutations); kept until all surfaces migrate off manual.
151
+ */
152
+ runRecorded<T>(work: () => Promise<T>): Promise<T>;
153
+ /**
154
+ * Record one entry onto the undo stack. Clears the redo stack. Fed by
155
+ * {@link flushBatch}/{@link endGroup} from the local-mutation stream, and
156
+ * still called directly by the legacy manual-record consumers
157
+ * (`useMutators`, the AI mutation pipeline) until they migrate. Entries are
158
+ * built internally (trusted), so the schema check is DEV-ONLY: it catches
159
+ * recorder bugs in dev/test (rejecting a malformed op at ingestion, with its
160
+ * path, instead of letting it crash later inside `applyOps`) without paying a
161
+ * Zod parse on every user action in production. The real validation boundary
162
+ * is `parseUndoEntry`, applied when entries are deserialized from persistence
163
+ * (untrusted input). Best practice: validate at trust boundaries, type-check
164
+ * internal calls.
165
+ */
87
166
  record(entry: UndoEntry): void;
167
+ /**
168
+ * Subscribe to every recorded mutation. Fires synchronously at the tail of
169
+ * each {@link record} call, after the entry is on the undo stack. Returns an
170
+ * unsubscribe function — call it on teardown.
171
+ *
172
+ * Listeners receive the full {@link UndoEntry} (its `forwards` carry the
173
+ * `{ kind, modelKey, data }` ops), so a consumer can derive what changed
174
+ * (e.g. "a slideLayers row of type 'chart' was created") without re-querying.
175
+ */
176
+ onRecord(listener: (entry: UndoEntry) => void): () => void;
177
+ private emitRecord;
88
178
  canUndo(): boolean;
89
179
  canRedo(): boolean;
90
- /** Pop the last mutator and apply its inverses. Pushes to redo. */
180
+ /**
181
+ * Pop the last mutator and apply its inverses. Pushes to redo.
182
+ *
183
+ * Under the default `skip-stale` policy the inverses are filtered against
184
+ * live state first (paired with the entry's forwards = "what I set"), so a
185
+ * field a collaborator changed after my op is left untouched — undo reverts
186
+ * my change only where it still stands.
187
+ */
91
188
  undo(): Promise<void>;
92
- /** Pop the last undone entry and re-apply the forward ops. Pushes to undo. */
189
+ /**
190
+ * Pop the last undone entry and re-apply the forward ops. Pushes to undo.
191
+ * Symmetric to {@link undo}: forwards are filtered against live state
192
+ * (paired with the entry's inverses = "what undo restored"), so redo
193
+ * re-asserts my change only where the undone value still stands.
194
+ */
93
195
  redo(): Promise<void>;
94
196
  /** Drop all history. Use after bootstrap / sync group change / sync error. */
95
197
  clear(): void;
@@ -98,6 +200,12 @@ export declare class UndoScope<S extends Schema> {
98
200
  undo: number;
99
201
  redo: number;
100
202
  };
203
+ /**
204
+ * Detach from the local-mutation stream and drop listeners. Scopes are
205
+ * cached for the store's lifetime by `UndoManager`, so this is mainly for
206
+ * tests and explicit teardown.
207
+ */
208
+ dispose(): void;
101
209
  }
102
210
  /**
103
211
  * Central registry of named undo scopes. One per-app instance, created once