@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.
- package/CHANGELOG.md +54 -1
- package/NOTICE +2 -2
- package/README.md +99 -78
- package/dist/BaseSyncedStore.d.ts +3 -2
- package/dist/agent/Agent.d.ts +1 -1
- package/dist/agent/Agent.js +1 -1
- package/dist/agent/index.d.ts +4 -4
- package/dist/agent/index.js +6 -6
- package/dist/agent/types.d.ts +1 -1
- package/dist/ai-sdk/index.d.ts +3 -3
- package/dist/ai-sdk/index.js +3 -3
- package/dist/ai-sdk/intent-broadcast.d.ts +1 -1
- package/dist/ai-sdk/intent-broadcast.js +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/client/Ablo.d.ts +53 -27
- package/dist/client/Ablo.js +32 -1
- package/dist/client/auth.d.ts +3 -3
- package/dist/client/auth.js +5 -5
- package/dist/client/createModelProxy.d.ts +118 -32
- package/dist/client/createModelProxy.js +87 -44
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.js +3 -3
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +2 -2
- package/dist/errors.d.ts +9 -7
- package/dist/errors.js +9 -7
- package/dist/index.d.ts +20 -6
- package/dist/index.js +41 -22
- package/dist/interfaces/headless.d.ts +1 -1
- package/dist/interfaces/headless.js +2 -2
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +2 -2
- package/dist/policy/types.d.ts +10 -0
- package/dist/principal.d.ts +3 -3
- package/dist/principal.js +3 -3
- package/dist/query/client.d.ts +7 -6
- package/dist/react/AbloProvider.d.ts +44 -1
- package/dist/react/AbloProvider.js +3 -1
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/SyncGroupProvider.js +1 -1
- package/dist/react/context.d.ts +1 -1
- package/dist/react/context.js +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutate.d.ts +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useReader.d.ts +1 -1
- package/dist/schema/field.d.ts +1 -1
- package/dist/schema/field.js +1 -1
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +2 -2
- package/dist/schema/model.d.ts +2 -2
- package/dist/schema/model.js +2 -2
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -1
- package/dist/schema/relation.d.ts +1 -1
- package/dist/schema/relation.js +1 -1
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.js +1 -1
- package/dist/source/index.d.ts +22 -28
- package/dist/source/index.js +23 -20
- package/dist/source/pushQueue.d.ts +1 -1
- package/dist/source/pushQueue.js +2 -2
- package/dist/sync/SyncWebSocket.d.ts +20 -5
- package/dist/sync/createIntentStream.js +7 -0
- package/dist/testing/fixtures/models.d.ts +1 -1
- package/dist/testing/fixtures/models.js +1 -1
- package/dist/testing/helpers/react-wrapper.d.ts +2 -2
- package/dist/testing/helpers/react-wrapper.js +2 -2
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/types/streams.d.ts +41 -1
- package/docs/api.md +78 -20
- package/docs/data-sources.md +50 -16
- package/docs/examples/ai-sdk-tool.md +14 -31
- package/docs/examples/existing-python-backend.md +6 -6
- package/docs/integration-guide.md +8 -7
- package/docs/interaction-model.md +16 -4
- package/docs/mcp.md +1 -1
- package/docs/quickstart.md +20 -18
- package/examples/data-source/README.md +1 -1
- package/examples/data-source/ablo-driver.ts +5 -5
- package/examples/data-source/customer-server.ts +10 -10
- package/examples/data-source/run.ts +9 -11
- package/examples/data-source/schema.ts +1 -1
- package/examples/quickstart.ts +2 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
package/docs/data-sources.md
CHANGED
|
@@ -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
|
|
66
|
-
|
|
|
67
|
-
|
|
|
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.
|
|
73
|
-
2. Store
|
|
74
|
-
3.
|
|
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
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
|
213
|
-
|
|
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
|
|
38
|
-
if (
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
}
|
|
68
|
-
await
|
|
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
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
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
|
-
|
|
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
|
|
104
|
+
Store the Ablo API key in the Python server:
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
|
-
|
|
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
|
-
|
|
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()
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
|
408
|
-
Your app
|
|
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
|
-
|
|
412
|
+
ABLO_API_KEY=sk_live_...
|
|
412
413
|
```
|
|
413
414
|
|
|
414
|
-
The
|
|
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` |
|
|
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.
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
| `
|
|
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.
|
package/docs/quickstart.md
CHANGED
|
@@ -43,8 +43,9 @@ export const ablo = Ablo({
|
|
|
43
43
|
});
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
});
|
|
99
|
+
const row = ablo.weatherReports.retrieve('weather_stockholm');
|
|
100
|
+
const weather = await weatherAgent.getWeather(row.location);
|
|
99
101
|
|
|
100
|
-
await
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
31
|
-
readonly
|
|
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
|
-
|
|
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
|
|
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
|
|
72
|
-
// Wrong
|
|
73
|
-
// function (instead of the env value directly) re-reads the
|
|
74
|
-
// on every request
|
|
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
|
-
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
77
|
+
apiKey: () => {
|
|
78
|
+
const apiKey = process.env.ABLO_API_KEY;
|
|
79
|
+
if (!apiKey) {
|
|
80
80
|
throw new Error(
|
|
81
|
-
'
|
|
81
|
+
'ABLO_API_KEY is not set — refusing to accept unsigned requests',
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
|
-
return
|
|
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
|
-
*
|
|
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
|
|
25
|
-
process.env.
|
|
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.
|
|
28
|
-
//
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
69
|
+
log('\n--- 5. signature failure (wrong API key) ---');
|
|
72
70
|
const badDriver = new AbloDriver({
|
|
73
71
|
handler: handleAbloSource,
|
|
74
|
-
|
|
72
|
+
apiKey: 'sk_test_wrong_key',
|
|
75
73
|
});
|
|
76
74
|
try {
|
|
77
75
|
await badDriver.load('tasks', 'task_seed');
|
package/examples/quickstart.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import Ablo from '@ablo
|
|
10
|
-
import { defineSchema, model, z } from '@ablo/
|
|
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,
|
|
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
|
|