@abloatai/ablo 0.3.1 → 0.5.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 (92) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/NOTICE +2 -2
  3. package/README.md +99 -78
  4. package/dist/BaseSyncedStore.d.ts +3 -2
  5. package/dist/agent/Agent.d.ts +1 -1
  6. package/dist/agent/Agent.js +1 -1
  7. package/dist/agent/index.d.ts +4 -4
  8. package/dist/agent/index.js +6 -6
  9. package/dist/agent/types.d.ts +1 -1
  10. package/dist/ai-sdk/index.d.ts +3 -3
  11. package/dist/ai-sdk/index.js +3 -3
  12. package/dist/ai-sdk/intent-broadcast.d.ts +1 -1
  13. package/dist/ai-sdk/intent-broadcast.js +1 -1
  14. package/dist/auth/index.d.ts +1 -1
  15. package/dist/client/Ablo.d.ts +53 -27
  16. package/dist/client/Ablo.js +32 -1
  17. package/dist/client/auth.d.ts +3 -3
  18. package/dist/client/auth.js +5 -5
  19. package/dist/client/createModelProxy.d.ts +118 -32
  20. package/dist/client/createModelProxy.js +87 -44
  21. package/dist/client/index.d.ts +3 -3
  22. package/dist/client/index.js +3 -3
  23. package/dist/config/index.d.ts +1 -1
  24. package/dist/config/index.js +1 -1
  25. package/dist/core/index.d.ts +1 -1
  26. package/dist/core/index.js +2 -2
  27. package/dist/errors.d.ts +9 -7
  28. package/dist/errors.js +9 -7
  29. package/dist/index.d.ts +20 -6
  30. package/dist/index.js +41 -22
  31. package/dist/interfaces/headless.d.ts +1 -1
  32. package/dist/interfaces/headless.js +2 -2
  33. package/dist/policy/index.d.ts +2 -2
  34. package/dist/policy/index.js +2 -2
  35. package/dist/policy/types.d.ts +10 -0
  36. package/dist/principal.d.ts +3 -3
  37. package/dist/principal.js +3 -3
  38. package/dist/query/client.d.ts +7 -6
  39. package/dist/react/AbloProvider.d.ts +44 -1
  40. package/dist/react/AbloProvider.js +3 -1
  41. package/dist/react/ClientSideSuspense.d.ts +1 -1
  42. package/dist/react/SyncGroupProvider.js +1 -1
  43. package/dist/react/context.d.ts +1 -1
  44. package/dist/react/context.js +1 -1
  45. package/dist/react/index.d.ts +1 -1
  46. package/dist/react/index.js +1 -1
  47. package/dist/react/useCurrentUserId.js +1 -1
  48. package/dist/react/useErrorListener.js +1 -1
  49. package/dist/react/useMutate.d.ts +1 -1
  50. package/dist/react/useMutationFailureListener.js +1 -1
  51. package/dist/react/useReader.d.ts +1 -1
  52. package/dist/schema/field.d.ts +1 -1
  53. package/dist/schema/field.js +1 -1
  54. package/dist/schema/index.d.ts +2 -2
  55. package/dist/schema/index.js +2 -2
  56. package/dist/schema/model.d.ts +2 -2
  57. package/dist/schema/model.js +2 -2
  58. package/dist/schema/queries.d.ts +1 -1
  59. package/dist/schema/queries.js +1 -1
  60. package/dist/schema/relation.d.ts +1 -1
  61. package/dist/schema/relation.js +1 -1
  62. package/dist/schema/schema.d.ts +1 -1
  63. package/dist/schema/schema.js +1 -1
  64. package/dist/source/index.d.ts +22 -28
  65. package/dist/source/index.js +23 -20
  66. package/dist/source/pushQueue.d.ts +1 -1
  67. package/dist/source/pushQueue.js +2 -2
  68. package/dist/sync/SyncWebSocket.d.ts +20 -5
  69. package/dist/sync/createIntentStream.js +7 -0
  70. package/dist/testing/fixtures/models.d.ts +1 -1
  71. package/dist/testing/fixtures/models.js +1 -1
  72. package/dist/testing/helpers/react-wrapper.d.ts +2 -2
  73. package/dist/testing/helpers/react-wrapper.js +2 -2
  74. package/dist/testing/index.d.ts +1 -1
  75. package/dist/testing/index.js +1 -1
  76. package/dist/types/streams.d.ts +41 -1
  77. package/docs/api.md +78 -20
  78. package/docs/data-sources.md +50 -16
  79. package/docs/examples/ai-sdk-tool.md +14 -31
  80. package/docs/examples/existing-python-backend.md +6 -6
  81. package/docs/integration-guide.md +8 -7
  82. package/docs/interaction-model.md +16 -4
  83. package/docs/mcp.md +1 -1
  84. package/docs/quickstart.md +20 -18
  85. package/examples/data-source/README.md +1 -1
  86. package/examples/data-source/ablo-driver.ts +5 -5
  87. package/examples/data-source/customer-server.ts +10 -10
  88. package/examples/data-source/run.ts +9 -11
  89. package/examples/data-source/schema.ts +1 -1
  90. package/examples/quickstart.ts +2 -2
  91. package/llms.txt +1 -1
  92. package/package.json +1 -1
@@ -2,6 +2,11 @@
2
2
 
3
3
  Every schema model has a backing store.
4
4
 
5
+ Customer apps must define an Ablo schema. The schema is the contract between
6
+ the SDK, agents, realtime subscriptions, and the Data Source endpoint. Use
7
+ `defineSchema`, `model`, and Zod the same way a Prisma project starts with a
8
+ `schema.prisma`.
9
+
5
10
  By default, Ablo stores the rows for the models you declare. That makes Ablo the
6
11
  managed state store for those resources, the same way Stripe stores `Customer`
7
12
  and `PaymentIntent` objects that you create through Stripe's API.
@@ -10,6 +15,11 @@ If you already have application tables and want those tables to remain
10
15
  canonical, attach a Data Source. Then Ablo coordinates the write and calls your
11
16
  app to commit it.
12
17
 
18
+ Your app can keep using its own `DATABASE_URL`. Store that value in your app or
19
+ backend environment, not in Ablo. The integration boundary is the HTTPS
20
+ endpoint your app exposes. The happy path uses the same server-side
21
+ `ABLO_API_KEY` to verify Ablo calls.
22
+
13
23
  Use the SDK with an API key:
14
24
 
15
25
  ```ts
@@ -24,6 +34,16 @@ export const ablo = Ablo({
24
34
 
25
35
  Do not pass a database URL to `Ablo(...)`.
26
36
 
37
+ For the first production integration, prefer this shape:
38
+
39
+ ```bash
40
+ # Stored only in your app/backend
41
+ DATABASE_URL=postgres://...
42
+
43
+ # The only Ablo credential in the customer app
44
+ ABLO_API_KEY=sk_live_...
45
+ ```
46
+
27
47
  ## Backing Modes
28
48
 
29
49
  | Mode | Where rows live | What `create/update/delete` does | Use when |
@@ -62,21 +82,18 @@ When you add a Data Source in Ablo, you get:
62
82
 
63
83
  | Field | Purpose |
64
84
  |---|---|
65
- | Data Source URL | The public HTTPS route in your app that Ablo will call. |
66
- | Signing secret | Stored in your app as `ABLO_DATA_SOURCE_SIGNING_SECRET`; used to verify Ablo calls. |
67
- | Push events URL | Ablo endpoint your app can call when rows change outside Ablo. |
85
+ | Data Source endpoint | The public HTTPS endpoint in your app that Ablo calls. |
86
+ | API key | Stored in your app as `ABLO_API_KEY`; used by the SDK and the Data Source endpoint. |
87
+ | External-write feed | Optional `events` handler on the same Data Source endpoint. |
68
88
  | Status | Last successful request, last error, and delivery attempts. |
69
89
 
70
90
  The shape is the same as a production webhook integration:
71
91
 
72
- 1. Add a Data Source URL in Ablo.
73
- 2. Store the signing secret in your app.
74
- 3. Expose one signed HTTP route from your app.
92
+ 1. Expose one Data Source endpoint in your app.
93
+ 2. Store `ABLO_API_KEY` in your app.
94
+ 3. Verify signed HTTP calls before opening a database transaction.
75
95
  4. Keep your database credentials in your app.
76
-
77
- ```bash
78
- ABLO_DATA_SOURCE_SIGNING_SECRET=whsec_...
79
- ```
96
+ 5. Write an outbox row when data changes outside Ablo.
80
97
 
81
98
  ## Route
82
99
 
@@ -88,7 +105,7 @@ import { db } from '@/db';
88
105
 
89
106
  export const POST = dataSource({
90
107
  schema,
91
- signingSecret: process.env.ABLO_DATA_SOURCE_SIGNING_SECRET,
108
+ apiKey: process.env.ABLO_API_KEY,
92
109
 
93
110
  authorize() {
94
111
  return { db };
@@ -176,7 +193,7 @@ handler so connected humans and agents stay current:
176
193
  ```ts
177
194
  export const POST = dataSource({
178
195
  schema,
179
- signingSecret: process.env.ABLO_DATA_SOURCE_SIGNING_SECRET,
196
+ apiKey: process.env.ABLO_API_KEY,
180
197
 
181
198
  async events({ cursor, limit, context }) {
182
199
  const page = await context.auth.db.outbox.after(cursor, { limit });
@@ -201,14 +218,31 @@ export const POST = dataSource({
201
218
  `clientTxId` lets Ablo drop SDK echoes that already produced a realtime update.
202
219
  Events without `clientTxId` are treated as external writes.
203
220
 
221
+ ## Production Checklist
222
+
223
+ Before using a customer-owned database in production:
224
+
225
+ - Keep `DATABASE_URL` in the customer app or backend environment.
226
+ - Use only the Data Source endpoint and `ABLO_API_KEY` as the customer-facing integration boundary.
227
+ - Verify signatures before opening a database transaction.
228
+ - Store `clientTxId` in an idempotency table before applying writes.
229
+ - Return canonical rows after each commit.
230
+ - Write outbox events in the same transaction as non-Ablo writes.
231
+ - Dedupe outbox events by event `id`.
232
+ - Monitor last success, last error, retry count, event lag, and cursor.
233
+
234
+ Do not send the customer's database URL to Ablo for this path. Direct database
235
+ URL custody would be a separate connector product with encrypted secret storage,
236
+ rotation, least-privilege roles, connection limits, table allowlists, and clear
237
+ data-processing terms.
238
+
204
239
  ## Security
205
240
 
206
- - Verify requests with `ABLO_DATA_SOURCE_SIGNING_SECRET`.
241
+ - Verify requests with `ABLO_API_KEY`.
207
242
  - Keep database credentials in your app.
208
243
  - Dedupe commits by `clientTxId`.
209
244
  - Dedupe external events by event `id`.
210
245
  - Use HTTPS in production.
211
246
 
212
- The signing secret is not a database credential and does not give Ablo access to
213
- your database. It only lets your route verify that the request came from Ablo
214
- and was not modified in transit.
247
+ The API key is not a database credential. It only lets your route verify that
248
+ the request came from Ablo and was not modified in transit.
@@ -34,38 +34,21 @@ const updateTask = tool({
34
34
  const [task] = await ablo.tasks.load({ where: { id: taskId } });
35
35
  if (!task) return { ok: false, reason: 'not_found' };
36
36
 
37
- const busy = ablo.intents.list({ resource: 'tasks', id: taskId });
38
- if (busy.length > 0) {
39
- await ablo.intents.waitFor(
40
- { resource: 'tasks', id: taskId },
41
- { timeout: 30_000 },
42
- );
43
- }
44
-
45
- const snap = ablo.snapshot({ tasks: taskId });
46
- const intent = await ablo.intents.create({
47
- target: { resource: 'tasks', id: taskId, field: 'status' },
48
- action: 'update',
49
- });
37
+ const claim = ablo.tasks.intent(taskId);
38
+ if (claim.current) await claim.whenFree({ timeout: 30_000 });
50
39
 
40
+ await claim.claim({ action: 'editing', field: 'status', ttl: '2m' });
51
41
  try {
52
- const updated = await ablo.tasks.update(
53
- taskId,
54
- {
55
- status: status ?? task.status,
56
- summary: summary ?? task.summary,
57
- },
58
- {
59
- intent,
60
- readAt: snap.stamp,
61
- onStale: 'reject',
62
- wait: 'confirmed',
63
- },
64
- );
42
+ // update commits with the held claim and auto-releases on success
43
+ const updated = await claim.update({
44
+ status: status ?? task.status,
45
+ summary: summary ?? task.summary,
46
+ });
65
47
 
66
48
  return { ok: true, task: updated };
67
- } finally {
68
- await intent.release();
49
+ } catch (err) {
50
+ await claim.finish();
51
+ throw err;
69
52
  }
70
53
  },
71
54
  });
@@ -85,8 +68,8 @@ The important part is not the model provider. The important part is that the
85
68
  tool:
86
69
 
87
70
  - loads the latest task,
88
- - checks active intent,
89
- - writes with `readAt`,
90
- - rejects stale state,
71
+ - waits if another participant already holds the row,
72
+ - acquires a claim before writing,
73
+ - writes and auto-releases through the same handle,
91
74
  - waits for server confirmation.
92
75
 
@@ -95,16 +95,16 @@ No string model key is needed in the first example. The selector reads from
95
95
 
96
96
  ## 3. Add One Python Data Source Endpoint
97
97
 
98
- In Ablo, configure the Data Source URL:
98
+ Expose one customer-owned Data Source endpoint:
99
99
 
100
100
  ```txt
101
101
  https://api.example.com/api/ablo/source
102
102
  ```
103
103
 
104
- Store the signing secret in the Python server:
104
+ Store the Ablo API key in the Python server:
105
105
 
106
106
  ```bash
107
- ABLO_DATA_SOURCE_SIGNING_SECRET=whsec_...
107
+ ABLO_API_KEY=sk_live_...
108
108
  ```
109
109
 
110
110
  Then expose one route that verifies the signed request and calls the existing
@@ -126,7 +126,7 @@ router = APIRouter()
126
126
 
127
127
 
128
128
  def verify_ablo_signature(request: Request, raw_body: bytes) -> None:
129
- secret = os.environ["ABLO_DATA_SOURCE_SIGNING_SECRET"].encode()
129
+ api_key = os.environ["ABLO_API_KEY"].encode()
130
130
  message_id = request.headers.get("webhook-id")
131
131
  timestamp = request.headers.get("webhook-timestamp")
132
132
  signature_header = request.headers.get("webhook-signature", "")
@@ -135,12 +135,12 @@ def verify_ablo_signature(request: Request, raw_body: bytes) -> None:
135
135
  raise HTTPException(status_code=401, detail="missing signature")
136
136
 
137
137
  signed_at = int(timestamp)
138
- if abs(int(time.time() * 1000) - signed_at) > 5 * 60 * 1000:
138
+ if abs(int(time.time()) - signed_at) > 5 * 60:
139
139
  raise HTTPException(status_code=401, detail="expired signature")
140
140
 
141
141
  payload = message_id.encode() + b"." + timestamp.encode() + b"." + raw_body
142
142
  expected = base64.b64encode(
143
- hmac.new(secret, payload, hashlib.sha256).digest()
143
+ hmac.new(api_key, payload, hashlib.sha256).digest()
144
144
  ).decode()
145
145
 
146
146
  presented = [
@@ -66,8 +66,8 @@ Every schema model has a backing store. The SDK call shape stays the same.
66
66
  | Schema-less resource API | Custom runtime | A server worker, MCP route, or migration script intentionally cannot import the app schema. |
67
67
 
68
68
  Do not pass a database URL to `Ablo(...)`. Application and agent code use
69
- `ABLO_API_KEY`. If your database stays canonical, add a Data Source URL in Ablo
70
- and keep the database credentials inside your app.
69
+ `ABLO_API_KEY`. If your database stays canonical, expose a signed Data Source
70
+ endpoint from your app and keep the database credentials inside your app.
71
71
 
72
72
  ## Test With Sandboxes
73
73
 
@@ -375,7 +375,7 @@ import { db } from '@/db';
375
375
 
376
376
  export const POST = dataSource({
377
377
  schema,
378
- signingSecret: process.env.ABLO_DATA_SOURCE_SIGNING_SECRET,
378
+ apiKey: process.env.ABLO_API_KEY,
379
379
 
380
380
  authorize() {
381
381
  return { db };
@@ -404,14 +404,15 @@ export const POST = dataSource({
404
404
  });
405
405
  ```
406
406
 
407
- Ablo gives you a Data Source URL, a signing secret, push/events URL, and status.
408
- Your app stores:
407
+ Ablo needs your Data Source endpoint and API key. External writes can be
408
+ reported through an optional `events` handler on the same route. Your app
409
+ stores one Ablo credential:
409
410
 
410
411
  ```bash
411
- ABLO_DATA_SOURCE_SIGNING_SECRET=whsec_...
412
+ ABLO_API_KEY=sk_live_...
412
413
  ```
413
414
 
414
- The signing secret verifies Ablo's request. It is not a database credential.
415
+ The API key verifies Ablo's request. It is not a database credential.
415
416
 
416
417
  ## 8. Agents
417
418
 
@@ -20,7 +20,7 @@ Capability -> Task -> Usage
20
20
  |---|---|---|
21
21
  | `Schema` | State | Declares typed models the app and agents can read and write. |
22
22
  | `Model` | State | The generated `ablo.<model>` resource. Use `load`, `retrieve`, `create`, `update`, and `delete`. |
23
- | `Intent` | State | Pre-write coordination. It says what this actor is preparing to change. |
23
+ | `Intent` | Coordination | Who is working on a target, as one Stripe-shaped object with a single `status`. Opened and read through `ablo.<model>.intent(id)`. Ephemeral — never persisted. |
24
24
  | `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
25
25
  | `Receipt` | Protocol | The lower-level durable result for custom runtimes. Schema writes use `wait: 'confirmed'`. |
26
26
  | `Capability` | Control | Signed credentials. It says who can do what, where, for how long, and on whose behalf. |
@@ -110,9 +110,21 @@ Removing any one of the three leaves a class of attack uncovered. The pattern ma
110
110
 
111
111
  ## Coordination
112
112
 
113
- Intents broadcast across the org. When `agent:task-writer` declares an intent to
114
- update a task, schema clients can see it through `ablo.intents.list(...)` or the
115
- live intent stream. Callers decide whether to yield, wait, or fail fast.
113
+ Intents broadcast across the org. Open and read one on any row through the
114
+ model accessor:
115
+
116
+ ```ts
117
+ const task = ablo.tasks.intent('task_123');
118
+ if (task.current) await task.whenFree(); // someone's working — wait
119
+ await task.claim({ action: 'editing' }); // claim so others yield
120
+ await task.update({ status: 'done' }); // commits + releases
121
+ ```
122
+
123
+ `task.current` is the live `Intent` (or `null`); `task.whenFree()` resolves when
124
+ the holder finishes. The same signal is visible to every schema client through
125
+ `ablo.intents.list(...)` and the live intent stream, so callers decide whether
126
+ to yield, wait, or fail fast. See [API → Intent](./api.md#intent) for the object
127
+ reference and lifecycle.
116
128
 
117
129
  ## Conflict resolution
118
130
 
package/docs/mcp.md CHANGED
@@ -21,7 +21,7 @@ Each resource you declare becomes one or more MCP tools:
21
21
  | `retrieve` | `<resource>.retrieve` | Returns the row + a stamp. |
22
22
  | `list` | `<resource>.list` | Cursor-paginated discovery. |
23
23
  | `update` | `<resource>.update` | Write, requires the prior stamp. |
24
- | `intents.create` | `intent.create` | Declare a claim before writing. |
24
+ | `<model>.intent` | `intent.create` | Claim a row before writing, then auto-release on update. |
25
25
 
26
26
  The assistant gets typed JSON schemas, real argument types, and typed
27
27
  rejections when it writes stale state. No invention, no hallucinated IDs.
@@ -43,8 +43,9 @@ export const ablo = Ablo({
43
43
  });
44
44
  ```
45
45
 
46
- Pass `schema` for typed model resources. Omit it only for advanced server-side
47
- resource clients such as custom agents and MCP routes.
46
+ Customer apps should always pass `schema`. Treat it like Prisma's schema file:
47
+ it is the source of truth for typed model resources, realtime subscriptions,
48
+ agent writes, and Data Source requests.
48
49
 
49
50
  ## 4. Create and Update
50
51
 
@@ -79,33 +80,34 @@ ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
79
80
 
80
81
  ## 6. AI Activity on Existing State
81
82
 
82
- Use `edit` when AI or background work will touch an existing row for more than a
83
- quick write. Other participants can see the activity while your code runs. The
84
- activity is cleared when `update` finishes; call `release` if the work ends
85
- without a write.
83
+ When AI or background work will touch an existing row for more than a quick
84
+ write, coordinate through `ablo.<model>.intent(id)`. It returns a handle
85
+ synchronously read `.current` to see who's working on the row, `claim()` to
86
+ claim it, `update()` to write under the claim (which auto-releases).
86
87
 
87
88
  ```ts
88
- const edit = await ablo.weatherReports.edit('weather_stockholm', {
89
- activity: 'checking_weather',
90
- field: 'forecast',
91
- ttl: '2m',
92
- });
89
+ const report = ablo.weatherReports.intent('weather_stockholm');
90
+
91
+ // If another participant holds it, wait for them to finish.
92
+ if (report.current) await report.whenFree();
93
+
94
+ // Claim it so other participants yield while we work.
95
+ await report.claim({ action: 'checking_weather', field: 'forecast', ttl: '2m' });
93
96
 
94
97
  // Your existing weather tool or agent call. While this runs, other clients see
95
98
  // that weather_stockholm is being checked.
96
- const weather = await weatherAgent.getWeather(edit.current.location, {
97
- signal: edit.signal,
98
- });
99
+ const row = ablo.weatherReports.retrieve('weather_stockholm');
100
+ const weather = await weatherAgent.getWeather(row.location);
99
101
 
100
- await edit.update({
102
+ await report.update({
101
103
  status: 'ready',
102
104
  forecast: weather.summary,
103
105
  });
104
106
  ```
105
107
 
106
- Ablo does not fetch the weather. It keeps the activity visible, gives the agent
107
- call an abort signal if the row changes, and clears the activity when
108
- `edit.update(...)` finishes.
108
+ Ablo does not fetch the weather. It keeps the activity visible while the work
109
+ runs, rejects `report.update(...)` with `AbloStaleContextError` if the row
110
+ changed under you, and releases the intent automatically once the write lands.
109
111
 
110
112
  ## 7. Multiplayer and Busy Work
111
113
 
@@ -22,7 +22,7 @@ npx tsx data-source/run.ts
22
22
 
23
23
  No network port, no env vars, no cloud credentials. The orchestrator
24
24
  calls the handler in-process. Signer and verifier still exchange
25
- signed bytes — flip the secret and you'll see a 401.
25
+ signed bytes — flip the API key and you'll see a 401.
26
26
 
27
27
  ## What it proves
28
28
 
@@ -7,7 +7,7 @@
7
7
  * locally without standing up the cloud.
8
8
  *
9
9
  * In production:
10
- * - Ablo Cloud holds the signing secret in its config
10
+ * - Ablo Cloud holds the API key in its config
11
11
  * - It signs each outbound POST with `signAbloSourceRequest`
12
12
  * - The customer's `dataSource(...)` handler verifies the signature
13
13
  * - The response feeds back into Ablo Cloud's hosted realtime layer
@@ -19,7 +19,7 @@
19
19
  import {
20
20
  signAbloSourceRequest,
21
21
  type Ablo,
22
- } from '@ablo/sync-engine';
22
+ } from '@abloatai/ablo';
23
23
 
24
24
  export interface AbloDriverOptions {
25
25
  /**
@@ -27,8 +27,8 @@ export interface AbloDriverOptions {
27
27
  * the handler directly so there's no http port to manage.
28
28
  */
29
29
  readonly handler: (request: Request) => Promise<Response>;
30
- /** Same secret the customer's `dataSource(...)` is configured with. */
31
- readonly signingSecret: string;
30
+ /** Same API key the customer's `dataSource(...)` is configured with. */
31
+ readonly apiKey: string;
32
32
  }
33
33
 
34
34
  export class AbloDriver {
@@ -68,7 +68,7 @@ export class AbloDriver {
68
68
  const body = JSON.stringify(payload);
69
69
  const messageId = `msg_${Date.now()}_${this.messageCounter}`;
70
70
  const signed = await signAbloSourceRequest({
71
- secret: this.options.signingSecret,
71
+ apiKey: this.options.apiKey,
72
72
  body,
73
73
  messageId,
74
74
  });
@@ -15,7 +15,7 @@
15
15
  * inside a transaction. The shape of the handlers stays identical.
16
16
  */
17
17
 
18
- import Ablo, { dataSource } from '@ablo/sync-engine';
18
+ import Ablo, { dataSource } from '@abloatai/ablo';
19
19
  import { schema } from './schema';
20
20
 
21
21
  type TaskRow = {
@@ -68,20 +68,20 @@ taskStore.set('task_seed', {
68
68
  export const handleAbloSource = dataSource({
69
69
  schema,
70
70
 
71
- // The signing secret pairs with what Ablo Cloud is configured with.
72
- // Wrong secret -> 401 with `source_signature_invalid`. Passing a
73
- // function (instead of the env value directly) re-reads the secret
74
- // on every request convenient for rotation, and required by the
71
+ // The API key pairs with what Ablo Cloud is configured with.
72
+ // Wrong key -> 401 with `source_signature_invalid`. Passing a
73
+ // function (instead of the env value directly) re-reads the key
74
+ // on every request and is required by the
75
75
  // example because `run.ts` configures the env after this module is
76
76
  // imported.
77
- signingSecret: () => {
78
- const secret = process.env.ABLO_DATA_SOURCE_SIGNING_SECRET;
79
- if (!secret) {
77
+ apiKey: () => {
78
+ const apiKey = process.env.ABLO_API_KEY;
79
+ if (!apiKey) {
80
80
  throw new Error(
81
- 'ABLO_DATA_SOURCE_SIGNING_SECRET is not set — refusing to accept unsigned requests',
81
+ 'ABLO_API_KEY is not set — refusing to accept unsigned requests',
82
82
  );
83
83
  }
84
- return secret;
84
+ return apiKey;
85
85
  },
86
86
 
87
87
  // `authorize` runs before any handler. Use it to map the signed
@@ -9,7 +9,7 @@
9
9
  * What this proves:
10
10
  *
11
11
  * 1. Ablo Cloud's signer + the customer's verifier interop. A wrong
12
- * secret produces `source_signature_invalid`.
12
+ * API key produces `source_signature_invalid`.
13
13
  * 2. `load`, `list`, `commit`, and `events` all flow through the
14
14
  * same Fetch-API handler.
15
15
  * 3. The customer's "database" (here a Map) holds canonical rows.
@@ -21,19 +21,17 @@
21
21
  import { handleAbloSource, _inspectStore } from './customer-server';
22
22
  import { AbloDriver } from './ablo-driver';
23
23
 
24
- const SIGNING_SECRET =
25
- process.env.ABLO_DATA_SOURCE_SIGNING_SECRET ?? 'whsec_example_secret_do_not_use_in_prod';
24
+ const API_KEY =
25
+ process.env.ABLO_API_KEY ?? 'sk_test_example_key_do_not_use_in_prod';
26
26
 
27
- // `dataSource()` reads `options.signingSecret` at construction; we
28
- // re-export the same value to the driver so signer and verifier agree.
29
- // In production this is one secret shared between Ablo Cloud config
30
- // and the customer's environment.
31
- process.env.ABLO_DATA_SOURCE_SIGNING_SECRET = SIGNING_SECRET;
27
+ // `dataSource()` reads `options.apiKey` at request time; we re-export
28
+ // the same value to the driver so signer and verifier agree.
29
+ process.env.ABLO_API_KEY = API_KEY;
32
30
 
33
31
  async function main() {
34
32
  const driver = new AbloDriver({
35
33
  handler: handleAbloSource,
36
- signingSecret: SIGNING_SECRET,
34
+ apiKey: API_KEY,
37
35
  });
38
36
 
39
37
  log('--- 1. load (existing seeded row) ---');
@@ -68,10 +66,10 @@ async function main() {
68
66
  const events = await driver.events();
69
67
  log('events:', events);
70
68
 
71
- log('\n--- 5. signature failure (wrong secret) ---');
69
+ log('\n--- 5. signature failure (wrong API key) ---');
72
70
  const badDriver = new AbloDriver({
73
71
  handler: handleAbloSource,
74
- signingSecret: 'whsec_wrong_secret',
72
+ apiKey: 'sk_test_wrong_key',
75
73
  });
76
74
  try {
77
75
  await badDriver.load('tasks', 'task_seed');
@@ -12,7 +12,7 @@
12
12
  * side without the other is a compile error.
13
13
  */
14
14
 
15
- import { defineSchema, model, z } from '@ablo/sync-engine/schema';
15
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
16
16
 
17
17
  export const schema = defineSchema({
18
18
  tasks: model({
@@ -6,8 +6,8 @@
6
6
  * ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
7
7
  */
8
8
 
9
- import Ablo from '@ablo/sync-engine';
10
- import { defineSchema, model, z } from '@ablo/sync-engine/schema';
9
+ import Ablo from '@abloatai/ablo';
10
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
11
11
 
12
12
  const schema = defineSchema({
13
13
  weatherReports: model({
package/llms.txt CHANGED
@@ -103,7 +103,7 @@ Teach schema as model fields and relations first. Advanced schema helpers such a
103
103
 
104
104
  Do not add `databaseURL` to `Ablo(...)`. Application and agent code use `ABLO_API_KEY`.
105
105
 
106
- Every schema model has a backing store. By default, Ablo stores rows for declared models, so `ablo.<model>.create/update/delete` write to Ablo-managed state. If the customer database is canonical, add a Data Source URL in Ablo, store `ABLO_DATA_SOURCE_SIGNING_SECRET` in the app, then expose a signed `dataSource({ schema, signingSecret, load, list, commit, events })` route from the customer app. Customer-owned app database credentials stay private.
106
+ Every schema model has a backing store. By default, Ablo stores rows for declared models, so `ablo.<model>.create/update/delete` write to Ablo-managed state. If the customer database is canonical, expose a Data Source endpoint and pass `apiKey: process.env.ABLO_API_KEY` to `dataSource({ schema, apiKey, load, list, commit, events })`. Customer-owned app database credentials stay private.
107
107
 
108
108
  Use `dataSource` from the root import:
109
109
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "State control API for AI agents and collaborative apps.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",