@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
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A Stripe-style webhook event delivered to the customer's endpoint. Verified
3
+ * (via the Standard Webhooks library) before the customer reads it.
4
+ */
5
+ export interface AbloWebhookEvent {
6
+ /** Stable event id = `String(syncId)`. Dedupe by this (idempotency). */
7
+ readonly id: string;
8
+ /** `<model>.<verb>`, e.g. `"slide.updated"` — switch on this. */
9
+ readonly type: string;
10
+ /** Wire model name, e.g. `"Slide"`. */
11
+ readonly model: string;
12
+ /** The changed row's id. */
13
+ readonly objectId: string;
14
+ /** Monotonic transaction-log position. ORDER by this (and dedupe). */
15
+ readonly syncId: number;
16
+ /** The post-change row (the object), or `null` on a delete. Like Stripe's
17
+ * `event.data.object`. */
18
+ readonly data: Record<string, unknown> | null;
19
+ /** ISO timestamp the change was committed. */
20
+ readonly createdAt: string;
21
+ }
22
+ /** The minimal delta shape the mapping reads (a `ServerSyncDelta` satisfies it). */
23
+ export interface WebhookSourceDelta {
24
+ readonly id: number;
25
+ readonly actionType: string;
26
+ readonly modelName: string;
27
+ readonly modelId: string;
28
+ /** `jsonb` — parsed object, raw JSON string, or null. */
29
+ readonly data: Record<string, unknown> | string | null;
30
+ readonly createdAt: string;
31
+ }
32
+ /**
33
+ * Map a committed delta to a customer-facing webhook event. Returns `null` for
34
+ * internal sync deltas (permission/group changes) that aren't customer events —
35
+ * the caller skips those (no webhook emitted). Pure: the `syncId` and timestamp
36
+ * come from the delta, so the mapping is deterministic.
37
+ */
38
+ export declare function deltaToWebhookEvent(delta: WebhookSourceDelta): AbloWebhookEvent | null;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The customer-facing verb per delta action. Only the CRUD-ish actions become
3
+ * webhook events; `C`overing / `G`roupAdded / `S`groupRemoved are internal sync
4
+ * mechanics (permission/visibility), NOT customer events → no webhook.
5
+ */
6
+ const ACTION_VERB = {
7
+ I: 'created',
8
+ U: 'updated',
9
+ D: 'deleted',
10
+ A: 'archived',
11
+ V: 'unarchived',
12
+ };
13
+ function parseRow(data) {
14
+ if (data == null)
15
+ return null;
16
+ if (typeof data === 'string') {
17
+ return data === '' ? null : JSON.parse(data);
18
+ }
19
+ return data;
20
+ }
21
+ /**
22
+ * Map a committed delta to a customer-facing webhook event. Returns `null` for
23
+ * internal sync deltas (permission/group changes) that aren't customer events —
24
+ * the caller skips those (no webhook emitted). Pure: the `syncId` and timestamp
25
+ * come from the delta, so the mapping is deterministic.
26
+ */
27
+ export function deltaToWebhookEvent(delta) {
28
+ const verb = ACTION_VERB[delta.actionType];
29
+ if (!verb)
30
+ return null; // C / G / S — internal sync mechanics, not a customer event
31
+ return {
32
+ id: String(delta.id),
33
+ type: `${delta.modelName.toLowerCase()}.${verb}`,
34
+ model: delta.modelName,
35
+ objectId: delta.modelId,
36
+ syncId: delta.id,
37
+ data: parseRow(delta.data),
38
+ createdAt: delta.createdAt,
39
+ };
40
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `@abloatai/ablo/webhooks` — the webhook event catalog + delta mapping.
3
+ *
4
+ * Customers import {@link AbloWebhookEvent} to type their handler; the server
5
+ * uses {@link deltaToWebhookEvent} to turn transaction-log deltas into events
6
+ * for Svix to deliver. Signature verification is NOT here — the customer uses
7
+ * the open Standard Webhooks library (`svix` / `standardwebhooks`), so Ablo
8
+ * ships no crypto.
9
+ */
10
+ export { deltaToWebhookEvent, type AbloWebhookEvent, type WebhookSourceDelta, } from './events.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `@abloatai/ablo/webhooks` — the webhook event catalog + delta mapping.
3
+ *
4
+ * Customers import {@link AbloWebhookEvent} to type their handler; the server
5
+ * uses {@link deltaToWebhookEvent} to turn transaction-log deltas into events
6
+ * for Svix to deliver. Signature verification is NOT here — the customer uses
7
+ * the open Standard Webhooks library (`svix` / `standardwebhooks`), so Ablo
8
+ * ships no crypto.
9
+ */
10
+ export { deltaToWebhookEvent, } from './events.js';
@@ -0,0 +1,34 @@
1
+ /** The canonical wire envelope — Stripe's error-object shape. Every HTTP error
2
+ * response and every structured frame error carries this exact set of keys,
3
+ * regardless of which route or transport produced it. */
4
+ export interface ErrorEnvelope {
5
+ readonly type: string;
6
+ readonly code?: string;
7
+ readonly param?: string;
8
+ readonly message: string;
9
+ readonly doc_url?: string;
10
+ readonly request_id?: string;
11
+ }
12
+ /** {@link AbloError} subclass → default HTTP status. The subclass is chosen to
13
+ * match status semantics (a validation error is a 400, a permission error a
14
+ * 403), so a throw site only picks the right class + code and the status
15
+ * follows — an explicit `httpStatus` is passed only when it diverges (e.g. a
16
+ * 404 on the base class, a 503 on AbloServerError). Mirrors the same table in
17
+ * apps/sync-server's self-contained `errors.ts`. */
18
+ export declare function statusForType(type: string): number;
19
+ /**
20
+ * Convert ANY thrown value into the canonical {@link ErrorEnvelope} plus an
21
+ * HTTP status. A typed {@link AbloError} is serialized via its own `toJSON`
22
+ * (so `code`/`param`/`doc_url`/structured `details` survive) and gets its
23
+ * status from an explicit `httpStatus` or, failing that, {@link statusForType}.
24
+ * Anything else degrades to a 500 `internal_error` envelope — never a bare
25
+ * framework "Internal Server Error" text body, and never a raw error string
26
+ * leaked onto the wire as an unregistered code.
27
+ *
28
+ * `requestId` is stamped into the body when the error didn't already carry one,
29
+ * so the response and the `x-request-id` header agree for support correlation.
30
+ */
31
+ export declare function errorEnvelope(err: unknown, requestId?: string): {
32
+ body: ErrorEnvelope;
33
+ status: number;
34
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ERROR egress — turn ANY thrown value into Stripe's error-object envelope plus
3
+ * an HTTP status, so every error response across the Ablo surface carries the
4
+ * identical `{ type, code, param, message, doc_url, request_id }` shape
5
+ * regardless of which route or service produced it.
6
+ *
7
+ * This is the wire-PRODUCE counterpart to `errors.ts`'s wire-PARSE
8
+ * (`translateHttpError`/`errorFromWire`). It lives in `wire/` — not in the main
9
+ * SDK entry — so a server-side consumer (a Next.js route) can import it without
10
+ * dragging in the client runtime (mobx/react/IndexedDB).
11
+ *
12
+ * The classifier is the UNIVERSAL baseline: a typed {@link AbloError} passes
13
+ * through (subclass + code + httpStatus preserved), everything else degrades to
14
+ * a 500 `internal_error`. Service-specific normalization that needs a DB driver
15
+ * (apps/sync-server classifies raw Postgres SQLSTATE + MutatorError) is layered
16
+ * on top in that service — it is intentionally NOT pulled into the shared,
17
+ * dependency-free contract.
18
+ */
19
+ import { AbloError, docUrlForCode } from '../errors.js';
20
+ import { errorCodeSpec } from '../errorCodes.js';
21
+ /** {@link AbloError} subclass → default HTTP status. The subclass is chosen to
22
+ * match status semantics (a validation error is a 400, a permission error a
23
+ * 403), so a throw site only picks the right class + code and the status
24
+ * follows — an explicit `httpStatus` is passed only when it diverges (e.g. a
25
+ * 404 on the base class, a 503 on AbloServerError). Mirrors the same table in
26
+ * apps/sync-server's self-contained `errors.ts`. */
27
+ export function statusForType(type) {
28
+ switch (type) {
29
+ case 'AbloAuthenticationError':
30
+ return 401;
31
+ case 'AbloPermissionError':
32
+ return 403;
33
+ case 'AbloValidationError':
34
+ return 400;
35
+ case 'AbloRateLimitError':
36
+ return 429;
37
+ case 'AbloIdempotencyError':
38
+ case 'AbloStaleContextError':
39
+ case 'AbloClaimedError':
40
+ return 409;
41
+ case 'AbloConnectionError':
42
+ return 503;
43
+ case 'AbloServerError':
44
+ return 500;
45
+ default:
46
+ return 500;
47
+ }
48
+ }
49
+ /**
50
+ * Convert ANY thrown value into the canonical {@link ErrorEnvelope} plus an
51
+ * HTTP status. A typed {@link AbloError} is serialized via its own `toJSON`
52
+ * (so `code`/`param`/`doc_url`/structured `details` survive) and gets its
53
+ * status from an explicit `httpStatus` or, failing that, {@link statusForType}.
54
+ * Anything else degrades to a 500 `internal_error` envelope — never a bare
55
+ * framework "Internal Server Error" text body, and never a raw error string
56
+ * leaked onto the wire as an unregistered code.
57
+ *
58
+ * `requestId` is stamped into the body when the error didn't already carry one,
59
+ * so the response and the `x-request-id` header agree for support correlation.
60
+ */
61
+ export function errorEnvelope(err, requestId) {
62
+ if (err instanceof AbloError) {
63
+ // Status precedence: an explicit httpStatus wins; else the code's canonical
64
+ // status from the registry (so `new AbloError('…', { code: 'entity_not_found' })`
65
+ // is a 404 without the throw site repeating it); else the subclass default.
66
+ const status = err.httpStatus ??
67
+ (err.code ? errorCodeSpec(err.code)?.httpStatus : undefined) ??
68
+ statusForType(err.type);
69
+ const body = err.toJSON();
70
+ return {
71
+ body: requestId && body.request_id === undefined ? { ...body, request_id: requestId } : body,
72
+ status,
73
+ };
74
+ }
75
+ const message = err instanceof Error ? err.message : String(err);
76
+ return {
77
+ body: {
78
+ type: 'AbloServerError',
79
+ code: 'internal_error',
80
+ message,
81
+ doc_url: docUrlForCode('internal_error'),
82
+ ...(requestId ? { request_id: requestId } : {}),
83
+ },
84
+ status: 500,
85
+ };
86
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `@abloatai/ablo/wire` — canonical COMMIT-PATH frame contract.
3
+ *
4
+ * These are the WebSocket (and HTTP-fallback) message shapes for the
5
+ * write path: the client's `commit` / `mutation` frames and the server's
6
+ * `mutation_result` ack. They live here — not in the server app and not
7
+ * inlined in the SDK's `SyncWebSocket` — so the client, the server, and
8
+ * any future `@abloatai/ablo/server` host all import ONE definition
9
+ * and cannot drift.
10
+ *
11
+ * Scope note: the delta/sync frames (`sync_response`, `delta`) are NOT
12
+ * here yet — they reference `SyncDelta`, which currently has two
13
+ * definitions (server `db/deltas` vs package `core`) pending unification.
14
+ * They stay server-local until that lands. Everything in this file
15
+ * depends only on package-canonical types (`OnStaleMode`, `ErrorCode`,
16
+ * `RequiredCapability`), so it is safe to share today.
17
+ *
18
+ * Changing any shape here is a wire-contract change — it requires
19
+ * coordinated client + server updates.
20
+ */
21
+ import type { OnStaleMode } from '../coordination/index.js';
22
+ import type { ErrorCode, RequiredCapability } from '../errors.js';
23
+ /**
24
+ * A single operation within a {@link CommitMessage} batch. The atomic unit
25
+ * the server's commit executor applies (and, once the mutator seam lands,
26
+ * the raw-op fallback path when no named mutator is registered).
27
+ */
28
+ export interface CommitOperation {
29
+ type: 'CREATE' | 'UPDATE' | 'DELETE' | 'ARCHIVE' | 'UNARCHIVE';
30
+ model: string;
31
+ id?: string | null;
32
+ input?: Record<string, unknown> | null;
33
+ /**
34
+ * Per-op client transaction id. Stamped onto `sync_deltas.transaction_id`
35
+ * so the originating client can recognize the broadcast as an echo of its
36
+ * own optimistic mutation. Distinct from the batch-level `clientTxId`
37
+ * (which keys `mutation_log` for retry idempotency).
38
+ */
39
+ transactionId?: string | null;
40
+ /**
41
+ * Watermark from `context.capture`. The server checks whether the target
42
+ * has received deltas since this id; if so the operation's `onStale` mode
43
+ * applies.
44
+ */
45
+ readAt?: number | null;
46
+ /**
47
+ * Mode on stale detection. `'reject'` (default) throws
48
+ * AbloStaleContextError; `'force'` applies unconditionally. `'flag'` /
49
+ * `'merge'` are reserved, not yet implemented.
50
+ */
51
+ onStale?: OnStaleMode | null;
52
+ }
53
+ /**
54
+ * Client → Server single named-mutation frame. The named-mutator write
55
+ * primitive (intent + args), as opposed to the raw-op {@link CommitMessage}
56
+ * batch. Server-side mutator dispatch resolves `mutatorName` against the
57
+ * host-provided registry.
58
+ */
59
+ export interface MutationMessage {
60
+ type: 'mutation';
61
+ payload: {
62
+ mutatorName: string;
63
+ input: unknown;
64
+ clientTxId: string;
65
+ };
66
+ }
67
+ /**
68
+ * Client → Server "commit this batch of operations" frame. Formerly named
69
+ * `batch_ack` / `BatchAckMessage` — renamed pre-stable to the customer-facing
70
+ * verb (`commit`) consistently across the wire and the SDK method
71
+ * (`MutationExecutor.commit`).
72
+ */
73
+ export interface CommitMessage {
74
+ type: 'commit';
75
+ payload: {
76
+ operations: CommitOperation[];
77
+ clientTxId: string;
78
+ /**
79
+ * Optional turn handle. When the SDK opens a turn via
80
+ * `SyncAgent.beginTurn(...)`, subsequent commits within the handle's
81
+ * scope auto-attach the `turnId` here. The Hub validates the turn
82
+ * belongs to the same agent and is open, then threads it onto every
83
+ * delta's `caused_by_task_id` column. Absent for human-direct commits
84
+ * and for SDKs that predate the turn protocol — those produce deltas
85
+ * with `caused_by_task_id = NULL`, which the audit pane treats as "no
86
+ * prompt-side context recorded."
87
+ */
88
+ causedByTaskId?: string | null;
89
+ };
90
+ }
91
+ /**
92
+ * Wire ack for a `commit` frame. Payload mirrors the canonical
93
+ * `CommitReceipt` shape so WebSocket, HTTP `/v1/commits`, and persisted
94
+ * `AgentJob.result.receipt` all carry identical fields.
95
+ *
96
+ * `object`, `status`, and `ops` are typed optional because pre-unification
97
+ * WS clients didn't ship them; servers always populate them on the way out.
98
+ * New clients can rely on them.
99
+ */
100
+ export interface MutationResultMessage {
101
+ type: 'mutation_result';
102
+ payload: {
103
+ object?: 'commit_receipt';
104
+ clientTxId: string;
105
+ serverTxId: string;
106
+ success: boolean;
107
+ status?: 'confirmed' | 'rejected';
108
+ lastSyncId?: number;
109
+ ops?: number;
110
+ error?: {
111
+ code: ErrorCode;
112
+ message: string;
113
+ field?: string;
114
+ /** Structured rejection body (x402-style) emitted when the cap
115
+ * verifier denies the commit. */
116
+ requiredCapability?: RequiredCapability;
117
+ };
118
+ };
119
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `@abloatai/ablo/wire` — the canonical HTTP/frame WIRE CONTRACT, with no
3
+ * client-runtime (mobx / react / IndexedDB) dependency, so a server-side
4
+ * consumer — a Next.js route handler, an edge function — can import the
5
+ * envelope producers without pulling in the whole sync client.
6
+ *
7
+ * Two halves, both Stripe-shaped and used across every Ablo surface:
8
+ * - ERROR egress — {@link errorEnvelope} / {@link ErrorEnvelope} /
9
+ * {@link statusForType} turn any thrown value into
10
+ * `{ type, code, param, message, doc_url, request_id }`.
11
+ * - LIST egress — {@link listEnvelope} / {@link ListEnvelope} stamp the
12
+ * uniform `{ object: 'list', data, has_more, next_cursor }` collection.
13
+ *
14
+ * The {@link AbloError} hierarchy + {@link docUrlForCode} + the wire-PARSE
15
+ * helpers are re-exported so a route can THROW the right typed error and
16
+ * SERIALIZE it through a single import.
17
+ */
18
+ export { errorEnvelope, statusForType } from './errorEnvelope.js';
19
+ export type { ErrorEnvelope } from './errorEnvelope.js';
20
+ export { listEnvelope } from './listEnvelope.js';
21
+ export type { ListEnvelope } from './listEnvelope.js';
22
+ export type { CommitOperation, MutationMessage, CommitMessage, MutationResultMessage, } from './frames.js';
23
+ export { AbloError, AbloAuthenticationError, AbloPermissionError, AbloValidationError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, SyncSessionError, docUrlForCode, translateHttpError, errorFromWire, toAbloError, ERROR_CONTRACT_VERSION, } from '../errors.js';
24
+ export type { ErrorCode, WireErrorCode } from '../errors.js';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `@abloatai/ablo/wire` — the canonical HTTP/frame WIRE CONTRACT, with no
3
+ * client-runtime (mobx / react / IndexedDB) dependency, so a server-side
4
+ * consumer — a Next.js route handler, an edge function — can import the
5
+ * envelope producers without pulling in the whole sync client.
6
+ *
7
+ * Two halves, both Stripe-shaped and used across every Ablo surface:
8
+ * - ERROR egress — {@link errorEnvelope} / {@link ErrorEnvelope} /
9
+ * {@link statusForType} turn any thrown value into
10
+ * `{ type, code, param, message, doc_url, request_id }`.
11
+ * - LIST egress — {@link listEnvelope} / {@link ListEnvelope} stamp the
12
+ * uniform `{ object: 'list', data, has_more, next_cursor }` collection.
13
+ *
14
+ * The {@link AbloError} hierarchy + {@link docUrlForCode} + the wire-PARSE
15
+ * helpers are re-exported so a route can THROW the right typed error and
16
+ * SERIALIZE it through a single import.
17
+ */
18
+ export { errorEnvelope, statusForType } from './errorEnvelope.js';
19
+ export { listEnvelope } from './listEnvelope.js';
20
+ // The error surface a wire consumer needs to throw, classify, and serialize.
21
+ export { AbloError, AbloAuthenticationError, AbloPermissionError, AbloValidationError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, SyncSessionError, docUrlForCode, translateHttpError, errorFromWire, toAbloError, ERROR_CONTRACT_VERSION, } from '../errors.js';
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The canonical Ablo LIST envelope — the one shape every endpoint that returns
3
+ * a collection uses, so a consumer can detect + paginate any list uniformly
4
+ * instead of learning a per-endpoint payload key (`{ keys }`, `{ origins }`,
5
+ * `{ events }`, `{ buckets }`…).
6
+ *
7
+ * `{ object: 'list', data: [...], has_more, next_cursor }` is the shape the
8
+ * hosted `GET /v1/models/:model` endpoint already emits (apps/sync-server
9
+ * `routes/query.ts`) and that `@ablo/mcp` already consumes — promoted here so
10
+ * sync-web's dashboard lists, the SDK, and any future surface produce the
11
+ * identical envelope from one definition.
12
+ *
13
+ * The field NAMES are Stripe's (`object`/`has_more`/`next_cursor`), not
14
+ * PlanetScale's (`type`/`cursor_start`/`has_next`): the rest of the Ablo API is
15
+ * Stripe-modeled, so this keeps one vocabulary across the surface. The
16
+ * PlanetScale discipline we deliberately borrow is *"every list is the same
17
+ * envelope"* — not the concrete key names.
18
+ */
19
+ export interface ListEnvelope<T> {
20
+ /** Discriminator — always `'list'`. Lets a generic client recognise a
21
+ * paginated collection without per-endpoint special-casing. */
22
+ readonly object: 'list';
23
+ /** The page of results. Always present (an empty array when there are none),
24
+ * never omitted, so `body.data` is a stable access path. */
25
+ readonly data: readonly T[];
26
+ /** Whether more results exist past this page. Drive "load more" off this,
27
+ * not off `data.length === limit` (ambiguous on an exact-multiple page). */
28
+ readonly has_more: boolean;
29
+ /** Opaque cursor to pass back as `?starting_after=` for the next page, or
30
+ * `null` when {@link has_more} is `false`. */
31
+ readonly next_cursor: string | null;
32
+ }
33
+ /**
34
+ * Stamp the uniform {@link ListEnvelope} onto an already-resolved page of rows.
35
+ *
36
+ * Pagination stays the caller's responsibility (fetch `limit + 1`, decide
37
+ * `hasMore`, derive the cursor from the last row's order key) — this only
38
+ * applies the envelope so no endpoint hand-rolls the shape. The defaults model
39
+ * the common "small, unpaginated collection" case (`has_more: false`,
40
+ * `next_cursor: null`); a paginated endpoint passes both explicitly.
41
+ */
42
+ export declare function listEnvelope<T>(data: readonly T[], opts?: {
43
+ hasMore?: boolean;
44
+ nextCursor?: string | null;
45
+ }): ListEnvelope<T>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Stamp the uniform {@link ListEnvelope} onto an already-resolved page of rows.
3
+ *
4
+ * Pagination stays the caller's responsibility (fetch `limit + 1`, decide
5
+ * `hasMore`, derive the cursor from the last row's order key) — this only
6
+ * applies the envelope so no endpoint hand-rolls the shape. The defaults model
7
+ * the common "small, unpaginated collection" case (`has_more: false`,
8
+ * `next_cursor: null`); a paginated endpoint passes both explicitly.
9
+ */
10
+ export function listEnvelope(data, opts = {}) {
11
+ return {
12
+ object: 'list',
13
+ data,
14
+ has_more: opts.hasMore ?? false,
15
+ next_cursor: opts.nextCursor ?? null,
16
+ };
17
+ }
package/docs/api.md CHANGED
@@ -31,10 +31,10 @@ const schema = defineSchema({
31
31
  const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
32
32
 
33
33
  await ablo.ready();
34
- const report = await ablo.weatherReports.retrieve('report_stockholm');
34
+ const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
35
35
  if (!report) throw new Error('Row not found');
36
36
 
37
- await ablo.weatherReports.update('report_stockholm', { status: 'ready' }, { wait: 'confirmed' });
37
+ await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }, wait: 'confirmed' });
38
38
  ```
39
39
 
40
40
  For end-to-end app setup across React, existing backends, Data Source, and
@@ -44,29 +44,29 @@ agents, read the [Integration Guide](./integration-guide.md).
44
44
 
45
45
  Each schema model becomes a typed model on the client:
46
46
 
47
- - `ablo.weatherReports.retrieve(id)` reads one row asynchronously (server read).
47
+ - `ablo.weatherReports.retrieve({ id })` reads one row asynchronously (server read).
48
48
  - `ablo.weatherReports.list({ where })` reads a collection asynchronously (server read).
49
49
  - `ablo.weatherReports.get(id)` reads one row synchronously from the local graph.
50
- - `ablo.weatherReports.create(data)` creates a row.
51
- - `ablo.weatherReports.update(id, data, options?)` updates a row.
52
- - `ablo.weatherReports.delete(id, options?)` deletes a row.
50
+ - `ablo.weatherReports.create({ data })` creates a row.
51
+ - `ablo.weatherReports.update({ id, data, ...options })` updates a row.
52
+ - `ablo.weatherReports.delete({ id, ...options })` deletes a row.
53
53
 
54
54
  `retrieve`/`list` and `get`/`getAll`/`getCount` are not aliases. Use
55
- `retrieve(id)` or `list({ where })` when the row may not be local yet — they
55
+ `retrieve({ id })` or `list({ where })` when the row may not be local yet — they
56
56
  hydrate pool → IndexedDB → network. Use `get(id)` / `getAll({ where })` /
57
57
  `getCount({ where })` for a cheap synchronous snapshot of what is already in
58
58
  the local graph.
59
59
 
60
60
  | Method | Returns | Use when |
61
61
  |---|---|---|
62
- | `retrieve(id)` | `Promise<T \| undefined>` | You need one row, hydrating from local store and server. |
62
+ | `retrieve({ id })` | `Promise<T \| undefined>` | You need one row, hydrating from local store and server. |
63
63
  | `list({ where })` | `Promise<T[]>` | You need to hydrate a collection from local store and server. |
64
64
  | `get(id)` | `T \| undefined` | You want a synchronous snapshot of one local row. |
65
65
  | `getAll(options?)` | `T[]` | You want a synchronous snapshot of a local collection. |
66
66
  | `getCount(options?)` | `number` | You want a synchronous count of local rows. |
67
- | `create(data, options?)` | `Promise<T>` | You want to create through the schema model. |
68
- | `update(id, data, options?)` | `Promise<T>` | You want to update through the schema model. |
69
- | `delete(id, options?)` | `Promise<void>` | You want to delete through the schema model. |
67
+ | `create({ data, ...options })` | `Promise<T>` | You want to create through the schema model. |
68
+ | `update({ id, data, ...options })` | `Promise<T>` | You want to update through the schema model. |
69
+ | `delete({ id, ...options })` | `Promise<void>` | You want to delete through the schema model. |
70
70
 
71
71
  `retrieve`, `list`, `create`, `update`, and `delete` are the main path — they go
72
72
  through the server. `get` / `getAll` / `getCount` are **synchronous reads**
@@ -79,11 +79,13 @@ Use `snapshot` when a write should reject if the row changed mid-flight:
79
79
  ```ts
80
80
  const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
81
81
 
82
- await ablo.weatherReports.update(
83
- 'report_stockholm',
84
- { status: 'ready' },
85
- { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
86
- );
82
+ await ablo.weatherReports.update({
83
+ id: 'report_stockholm',
84
+ data: { status: 'ready' },
85
+ readAt: snap.stamp,
86
+ onStale: 'reject',
87
+ wait: 'confirmed',
88
+ });
87
89
  ```
88
90
 
89
91
  Protected write options:
@@ -105,11 +107,11 @@ two writers serialize instead of clobbering. A claim is temporary: it expires
105
107
  on its own if the holder stops, and is never saved as a row.
106
108
 
107
109
  You coordinate a row with calls on its model, beside `create`/`update`/`retrieve`:
108
- `ablo.<model>.claim(id, work)` takes the claim and runs your work,
109
- `ablo.<model>.claim.state(id)` reads who currently holds it (synchronous, never
110
- blocks), and `ablo.<model>.claim.release(id)` releases it early. The full
111
- coordination surface is `claim.state(id)` / `claim.queue(id)` /
112
- `claim.release(id)` / `claim.reorder(id, order)` hanging off `claim`.
110
+ `ablo.<model>.claim({ id })` takes the claim and returns a handle,
111
+ `ablo.<model>.claim.state({ id })` reads who currently holds it (synchronous, never
112
+ blocks), and `ablo.<model>.claim.release({ id })` releases it early. The full
113
+ coordination surface is `claim.state({ id })` / `claim.queue({ id })` /
114
+ `claim.release({ id })` / `claim.reorder({ id, order })` hanging off `claim`.
113
115
 
114
116
  ### The Claim State Object
115
117
 
@@ -140,7 +142,7 @@ coordination surface is `claim.state(id)` / `claim.queue(id)` /
140
142
  ### Lifecycle
141
143
 
142
144
  ```
143
- claim(id) update(id) lands
145
+ claim({ id }) update({ id }) lands
144
146
  (free) ───────────▶ active ───────────────────────▶ committed
145
147
 
146
148
  ┌───────────┴───────────┐
@@ -149,16 +151,16 @@ coordination surface is `claim.state(id)` / `claim.queue(id)` /
149
151
  (release w/o write) (TTL; holder died)
150
152
  ```
151
153
 
152
- A target is free when `ablo.<model>.claim.state(id)` is `null`. Terminal
154
+ A target is free when `ablo.<model>.claim.state({ id })` is `null`. Terminal
153
155
  states drop out of the live stream, so a present claim is either `active` (the
154
156
  holder) or `queued` (waiting in the FIFO line behind the holder; see
155
- `claim.queue(id)`).
157
+ `claim.queue({ id })`).
156
158
 
157
159
  ### Reading and claiming
158
160
 
159
- `claim.state(id)` is the read side for observers: synchronous, never blocks, and
160
- returns the live claim state object (or `null`). `claim(id, work)` is the write
161
- side: it takes the claim and returns the row. Claims don't lock — if someone else
161
+ `claim.state({ id })` is the read side for observers: synchronous, never blocks, and
162
+ returns the live claim state object (or `null`). `claim({ id })` is the write
163
+ side: it takes the claim and returns a `ClaimHandle`. Claims don't lock — if someone else
162
164
  already holds the row, `claim` waits for them to finish, re-reads the fresh row,
163
165
  then hands it to you, so you always proceed from current state. Default reads
164
166
  return the row even while someone is mid-edit; if a server read should not
@@ -166,31 +168,32 @@ return a row while it's claimed, pass `ifClaimed: 'wait'` to wait for the claim
166
168
  to clear, or `ifClaimed: 'fail'` to error out instead.
167
169
 
168
170
  ```ts
169
- const claim = ablo.weatherReports.claim.state('report_stockholm');
171
+ const claim = ablo.weatherReports.claim.state({ id: 'report_stockholm' });
170
172
  if (claim) {
171
173
  claim.heldBy;
172
174
  claim.action;
173
175
  }
174
176
 
175
- const updated = await ablo.weatherReports.claim(
176
- 'report_stockholm',
177
- async (report) => ablo.weatherReports.update(report.id, { status: 'ready' }),
178
- { action: 'editing', ttl: '2m' },
179
- );
177
+ const handle = await ablo.weatherReports.claim({
178
+ id: 'report_stockholm',
179
+ action: 'editing',
180
+ ttl: '2m',
181
+ });
182
+ await ablo.weatherReports.update({ id: handle.data.id, data: { status: 'ready' } });
183
+ await handle.release();
180
184
  ```
181
185
 
182
- Writes go through the normal `ablo.<model>.update(id, data)`. While you hold
186
+ Writes go through the normal `ablo.<model>.update({ id, data })`. While you hold
183
187
  a claim on `id`, that `update` rejects with `AbloStaleContextError` if the row
184
188
  changed underneath you since you took the claim, so you re-read before retrying.
185
- The callback form releases the claim automatically when the callback returns or
186
- throws; call `ablo.weatherReports.claim.release(id)` if you claimed manually and
187
- need to release early.
189
+ Call `handle.release()` (or `ablo.weatherReports.claim.release({ id })`) to release
190
+ the claim when your work is done.
188
191
 
189
192
  ## Agent
190
193
 
191
194
  Most agents should import the same schema as the app and call
192
- `ablo.<model>.list(...)`, `ablo.<model>.claim(...)`, and
193
- `ablo.<model>.update(...)`.
195
+ `ablo.<model>.list(...)`, `ablo.<model>.claim({ id })`, and
196
+ `ablo.<model>.update({ id, data })`.
194
197
 
195
198
  ## HTTP API
196
199
 
@@ -203,12 +206,12 @@ endpoint documents that model's real field contract instead of a generic blob.
203
206
 
204
207
  | SDK call | HTTP |
205
208
  |---|---|
206
- | `ablo.<model>.create(data)` | `POST /v1/models/{model}` |
209
+ | `ablo.<model>.create({ data })` | `POST /v1/models/{model}` |
207
210
  | `ablo.<model>.list({ where })` | `GET /v1/models/{model}` |
208
- | `ablo.<model>.retrieve(id)` | `GET /v1/models/{model}/{id}` |
209
- | `ablo.<model>.update(id, data)` | `PATCH /v1/models/{model}/{id}` |
210
- | `ablo.<model>.delete(id)` | `DELETE /v1/models/{model}/{id}` |
211
- | `ablo.<model>.claim(id)` | `POST /v1/models/{model}/{id}/claim` |
211
+ | `ablo.<model>.retrieve({ id })` | `GET /v1/models/{model}/{id}` |
212
+ | `ablo.<model>.update({ id, data })` | `PATCH /v1/models/{model}/{id}` |
213
+ | `ablo.<model>.delete({ id })` | `DELETE /v1/models/{model}/{id}` |
214
+ | `ablo.<model>.claim({ id })` | `POST /v1/models/{model}/{id}/claim` |
212
215
  | (release a claim) | `DELETE /v1/models/{model}/{id}/claim` |
213
216
 
214
217
  Auth is a bearer API key: `Authorization: Bearer sk_…`. Mutations take an