@abloatai/ablo 0.9.1 → 0.9.2
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/AGENTS.md +84 -0
- package/CHANGELOG.md +34 -0
- package/README.md +14 -6
- package/dist/BaseSyncedStore.js +6 -2
- package/dist/SyncClient.d.ts +12 -0
- package/dist/SyncClient.js +15 -0
- package/dist/agent/index.js +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/client/Ablo.d.ts +7 -49
- package/dist/client/Ablo.js +2 -104
- package/dist/client/ApiClient.d.ts +1 -113
- package/dist/client/ApiClient.js +0 -232
- package/dist/client/auth.js +32 -2
- package/dist/client/httpClient.d.ts +5 -6
- package/dist/client/httpClient.js +2 -3
- package/dist/client/index.d.ts +1 -1
- package/dist/errorCodes.js +3 -3
- package/dist/index.js +1 -1
- package/dist/interfaces/index.d.ts +4 -4
- package/dist/mutators/UndoManager.d.ts +17 -0
- package/dist/mutators/UndoManager.js +53 -0
- package/dist/react/AbloProvider.d.ts +18 -8
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/useUndoScope.js +7 -0
- package/dist/server/commit.d.ts +4 -5
- package/dist/types/streams.d.ts +2 -1
- package/dist/wire/frames.d.ts +6 -8
- package/docs/api.md +1 -1
- package/docs/cli.md +17 -4
- package/docs/data-sources.md +68 -83
- package/docs/examples/ai-sdk-tool.md +11 -5
- package/docs/examples/existing-python-backend.md +26 -4
- package/docs/examples/nextjs.md +3 -2
- package/docs/examples/scoped-agent.md +38 -11
- package/docs/identity.md +86 -59
- package/docs/integration-guide.md +85 -54
- package/docs/react.md +39 -28
- package/llms.txt +18 -11
- package/package.json +2 -2
package/dist/server/commit.d.ts
CHANGED
|
@@ -61,11 +61,10 @@ export interface CommitContext {
|
|
|
61
61
|
*/
|
|
62
62
|
confirmationState?: ConfirmationState;
|
|
63
63
|
/**
|
|
64
|
-
* FK to
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* predate the turn protocol (→ `caused_by_task_id = NULL`).
|
|
64
|
+
* Dormant FK to the agent-task id (`agent_tasks.id`). The SDK no longer
|
|
65
|
+
* sets it (turns/tasks removed; attribution rides on the claim/intent id
|
|
66
|
+
* + server-stamped actor/capability). Still validated + written onto
|
|
67
|
+
* `caused_by_task_id` when present, but client writes leave it `null`.
|
|
69
68
|
*/
|
|
70
69
|
causedByTaskId?: string | null;
|
|
71
70
|
}
|
package/dist/types/streams.d.ts
CHANGED
|
@@ -52,7 +52,8 @@ export interface AgentDelta {
|
|
|
52
52
|
capabilityId?: string | null;
|
|
53
53
|
/** Whether the human explicitly approved the change. */
|
|
54
54
|
confirmationState?: ConfirmationState | null;
|
|
55
|
-
/**
|
|
55
|
+
/** Agent-task id that caused this commit, if any. Dormant on new client
|
|
56
|
+
* writes (turns/tasks removed); may hold historical values. */
|
|
56
57
|
causedByTaskId?: string | null;
|
|
57
58
|
createdAt: string;
|
|
58
59
|
}
|
package/dist/wire/frames.d.ts
CHANGED
|
@@ -76,14 +76,12 @@ export interface CommitMessage {
|
|
|
76
76
|
operations: CommitOperation[];
|
|
77
77
|
clientTxId: string;
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* with `caused_by_task_id = NULL`, which the audit pane treats as "no
|
|
86
|
-
* prompt-side context recorded."
|
|
79
|
+
* Dormant agent-task lineage field. The SDK no longer populates it —
|
|
80
|
+
* turns/tasks were removed and write attribution now rides on the
|
|
81
|
+
* claim (`intent`) id plus the server-stamped actor/capability. Kept
|
|
82
|
+
* optional for wire-compat; when present the Hub still validates and
|
|
83
|
+
* threads it onto `caused_by_task_id`, but client writes leave it
|
|
84
|
+
* `null` (the audit pane treats null as "no prompt-side context").
|
|
87
85
|
*/
|
|
88
86
|
causedByTaskId?: string | null;
|
|
89
87
|
};
|
package/docs/api.md
CHANGED
|
@@ -8,7 +8,7 @@ row, you can optionally `claim` it so they serialize instead of clobbering
|
|
|
8
8
|
each other.
|
|
9
9
|
|
|
10
10
|
Two things to know before the method list. **Reads come in two flavors:**
|
|
11
|
-
`retrieve(id)` / `list({ where })` are async and hit the server (use them when
|
|
11
|
+
`retrieve({ id })` / `list({ where })` are async and hit the server (use them when
|
|
12
12
|
the row may not be local yet); `get(id)` / `getAll({ where })` / `getCount({ where })`
|
|
13
13
|
are synchronous reads off the local graph (use them in render, after data has
|
|
14
14
|
synced). **Claims don't lock.** If another writer holds the row, `claim` waits
|
package/docs/cli.md
CHANGED
|
@@ -57,7 +57,7 @@ either mode) defines the same models test and live see; only the rows differ.
|
|
|
57
57
|
| `ablo dev` | **Hosted** — push the schema to your test sandbox, then watch `ablo/schema.ts` and re-push on save. | `--no-watch`, `--schema <path>`, `--export <name>`, `--url <url>` |
|
|
58
58
|
| `ablo logs` | Tail your scope's commit activity (`stripe logs tail`). Follows by default. | `-n, --tail <N>`, `--since <dur\|ts>`, `--model`, `--op`, `--json`, `--no-follow`, `--mode test\|live` |
|
|
59
59
|
| `ablo push` | **Hosted** — upload the schema to Ablo; the server diffs, migrates, and activates it. | `--force`, `--rename old:new`, `--backfill model.field=value`, `--schema`, `--export`, `--url` |
|
|
60
|
-
| `ablo migrate` | **Direct Postgres** —
|
|
60
|
+
| `ablo migrate` | **Direct Postgres** — provision just the synced models (plus the adapter's `ablo_outbox` / `ablo_idempotency`) in your own `DATABASE_URL`. Leaves your other tables alone. | `--dry-run`, `--output <file>`, `--schema`, `--export` |
|
|
61
61
|
| `ablo pull` | **Direct Postgres** — generate `defineSchema(...)` from your existing tables (read-only, like `prisma db pull`). | `--out <path>`, `--app-schema <name>`, `--import <pkg>`, `--force` |
|
|
62
62
|
| `ablo check` | **Direct Postgres** — verify your _existing_ tables fit the schema (read-only, no schema changes). | `--schema <path>`, `--export <name>`, `--app-schema <name>` |
|
|
63
63
|
| `ablo generate` | Emit TypeScript types from the schema. | `--out <path>`, `--schema`, `--export` |
|
|
@@ -144,9 +144,9 @@ reshaping it. `ablo check` is read-only; it never proposes a migration.
|
|
|
144
144
|
## `migrate` (Direct Postgres) vs `push` (Hosted)
|
|
145
145
|
|
|
146
146
|
Same engine, two setups. If you use the **Direct Postgres connector**, use
|
|
147
|
-
`ablo migrate` — it
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
`ablo migrate` — it provisions the synced models in your own `DATABASE_URL`. If
|
|
148
|
+
Ablo manages the sandbox/hosted store, use `ablo push` and `ablo dev` — the
|
|
149
|
+
server applies the change and version-gates connecting clients.
|
|
150
150
|
|
|
151
151
|
```bash
|
|
152
152
|
ablo migrate --dry-run # preview the exact SQL
|
|
@@ -154,6 +154,19 @@ ablo migrate # apply to DATABASE_URL
|
|
|
154
154
|
ablo migrate --output schema.sql # write SQL to a file
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
+
### One database, two schemas
|
|
158
|
+
|
|
159
|
+
`ablo migrate` does **not** own your whole database. It creates exactly the
|
|
160
|
+
models in your `defineSchema(...)` — the synced, collaborative tables — plus the
|
|
161
|
+
adapter's bookkeeping tables (`ablo_outbox`, `ablo_idempotency`). Nothing else.
|
|
162
|
+
|
|
163
|
+
Your auth, billing, and any other non-synced tables stay in **your own ORM
|
|
164
|
+
schema** (Drizzle's `schema.ts`, Prisma's `schema.prisma`) and are provisioned by
|
|
165
|
+
**your own migrations** (`drizzle-kit push` / `prisma migrate`). The Ablo schema
|
|
166
|
+
is not a replacement for `schema.prisma`, and `ablo migrate` won't touch, drop,
|
|
167
|
+
or adopt the tables it doesn't manage. One database, two schemas, side by side —
|
|
168
|
+
each owned by its own migration tool.
|
|
169
|
+
|
|
157
170
|
## Zod → Postgres type mapping
|
|
158
171
|
|
|
159
172
|
The one type map, shared by both paths (there is no second mapping):
|
package/docs/data-sources.md
CHANGED
|
@@ -10,10 +10,16 @@ That default makes Ablo the managed state store for your models, the same way
|
|
|
10
10
|
Stripe stores `Customer` and `PaymentIntent` objects that you create through
|
|
11
11
|
Stripe's API.
|
|
12
12
|
|
|
13
|
-
Either way, you define an Ablo schema with `defineSchema`, `model`, and Zod
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
Either way, you define an Ablo schema with `defineSchema`, `model`, and Zod. The
|
|
14
|
+
Ablo schema describes **only your synced, collaborative models** — the rows Ablo
|
|
15
|
+
coordinates and fans out in realtime. It is *not* your whole-database schema and
|
|
16
|
+
does *not* replace your `schema.prisma` (or your Drizzle schema). Your auth,
|
|
17
|
+
billing, and any other non-synced tables stay in your own ORM schema, owned by
|
|
18
|
+
your own migrations. One database, two schemas, side by side: Ablo owns the
|
|
19
|
+
synced models (plus the small `ablo_outbox` / `ablo_idempotency` bookkeeping
|
|
20
|
+
tables its adapter needs); you keep owning everything else. `ablo check` reflects
|
|
21
|
+
this — it reports your other tables as "ignored / owned by you," which is exactly
|
|
22
|
+
right.
|
|
17
23
|
|
|
18
24
|
Your app can keep using its own `DATABASE_URL`. Store that value in your app or
|
|
19
25
|
backend environment, not in Ablo. The integration boundary is the HTTPS
|
|
@@ -66,8 +72,8 @@ Multiplayer behavior is the same in both modes. Writes made through
|
|
|
66
72
|
fan out to subscribers. If something writes to your database without going
|
|
67
73
|
through Ablo (a cron job, an admin tool), Ablo can't know about it
|
|
68
74
|
automatically. To keep everyone's screen up to date, your app reports those
|
|
69
|
-
outside changes back through
|
|
70
|
-
[
|
|
75
|
+
outside changes back through the outbox feed — shown below in
|
|
76
|
+
[Outbox Events](#outbox-events).
|
|
71
77
|
|
|
72
78
|
## When To Use A Data Source
|
|
73
79
|
|
|
@@ -100,59 +106,53 @@ The shape is the same as a production webhook integration:
|
|
|
100
106
|
|
|
101
107
|
## Route
|
|
102
108
|
|
|
109
|
+
You don't hand-write the commit transaction, the idempotency upsert, or the
|
|
110
|
+
outbox writes. You pass an ORM **adapter** and it does all of that for you —
|
|
111
|
+
transaction, exactly-once idempotency, and outbox — driven by the same Ablo
|
|
112
|
+
schema. The whole route is three fields:
|
|
113
|
+
|
|
103
114
|
```ts
|
|
104
115
|
// app/api/ablo/source/route.ts
|
|
105
|
-
import {
|
|
116
|
+
import { dataSourceNext } from '@abloatai/ablo/source/next';
|
|
117
|
+
import { prismaDataSource } from '@abloatai/ablo/source';
|
|
106
118
|
import { schema } from '@/ablo/schema';
|
|
107
|
-
import {
|
|
119
|
+
import { prisma } from '@/lib/prisma';
|
|
108
120
|
|
|
109
|
-
|
|
121
|
+
// Data Source routes touch the database, so they run on the Node runtime.
|
|
122
|
+
export const runtime = 'nodejs';
|
|
123
|
+
|
|
124
|
+
export const { POST } = dataSourceNext({
|
|
110
125
|
schema,
|
|
111
|
-
apiKey: process.env.ABLO_API_KEY
|
|
126
|
+
apiKey: process.env.ABLO_API_KEY!,
|
|
127
|
+
adapter: prismaDataSource(prisma, schema),
|
|
128
|
+
});
|
|
129
|
+
```
|
|
112
130
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
},
|
|
131
|
+
Using Drizzle instead of Prisma is the same shape — swap the adapter for
|
|
132
|
+
`drizzleDataSource(db, schema)`:
|
|
116
133
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
sourceEventForOperation({
|
|
124
|
-
eventId,
|
|
125
|
-
operation,
|
|
126
|
-
entityId,
|
|
127
|
-
data,
|
|
128
|
-
...(clientTxId ? { clientTxId } : {}),
|
|
129
|
-
...(context.scope?.organizationId
|
|
130
|
-
? { organizationId: context.scope.organizationId }
|
|
131
|
-
: {}),
|
|
132
|
-
occurredAt: Date.now(),
|
|
133
|
-
}),
|
|
134
|
-
),
|
|
135
|
-
});
|
|
136
|
-
return changes.map(({ row }) => row);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
return { rows };
|
|
140
|
-
},
|
|
134
|
+
```ts
|
|
135
|
+
// app/api/ablo/source/route.ts
|
|
136
|
+
import { dataSourceNext } from '@abloatai/ablo/source/next';
|
|
137
|
+
import { drizzleDataSource } from '@abloatai/ablo/source/drizzle';
|
|
138
|
+
import { schema } from '@/ablo/schema';
|
|
139
|
+
import { db } from '@/db';
|
|
141
140
|
|
|
142
|
-
|
|
143
|
-
async load({ id, context }) {
|
|
144
|
-
return context.auth.db.report.findUnique({ where: { id } });
|
|
145
|
-
},
|
|
141
|
+
export const runtime = 'nodejs';
|
|
146
142
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
},
|
|
152
|
-
},
|
|
143
|
+
export const { POST } = dataSourceNext({
|
|
144
|
+
schema,
|
|
145
|
+
apiKey: process.env.ABLO_API_KEY!,
|
|
146
|
+
adapter: drizzleDataSource(db, schema),
|
|
153
147
|
});
|
|
154
148
|
```
|
|
155
149
|
|
|
150
|
+
The adapter is constructed from your ORM client and the Ablo `schema` —
|
|
151
|
+
`prismaDataSource(prisma, schema)` or `drizzleDataSource(db, schema)`. It maps
|
|
152
|
+
each synced model to your table, wraps every commit in one transaction, dedupes
|
|
153
|
+
on `clientTxId` via `ablo_idempotency`, and appends `ablo_outbox` rows for the
|
|
154
|
+
external-write feed — the bookkeeping you used to write by hand.
|
|
155
|
+
|
|
156
156
|
Your app code still writes through the normal model API:
|
|
157
157
|
|
|
158
158
|
```ts
|
|
@@ -208,37 +208,18 @@ events.
|
|
|
208
208
|
|
|
209
209
|
## Outbox Events
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
The adapter serves the outbox feed for you. Every `commit` it runs appends one
|
|
212
|
+
`ablo_outbox` row per operation in the same transaction, and the adapter's
|
|
213
|
+
built-in events handler streams those rows back to Ablo by cursor — so connected
|
|
214
|
+
humans and agents stay current with no extra code. If Ablo already appended the
|
|
215
|
+
commit directly, `clientTxId` lets Ablo filter the echo; if the direct append
|
|
216
|
+
failed, the same outbox row repairs it on the next poll or push.
|
|
215
217
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
async events({ cursor, limit, context }) {
|
|
222
|
-
const page = await context.auth.db.outbox.after(cursor, { limit });
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
events: page.rows.map((row) => ({
|
|
226
|
-
id: row.id,
|
|
227
|
-
model: row.model,
|
|
228
|
-
entityId: row.entityId,
|
|
229
|
-
type: row.type,
|
|
230
|
-
data: row.data,
|
|
231
|
-
organizationId: row.organizationId,
|
|
232
|
-
clientTxId: row.clientTxId,
|
|
233
|
-
occurredAt: row.createdAt.getTime(),
|
|
234
|
-
})),
|
|
235
|
-
nextCursor: page.nextCursor,
|
|
236
|
-
};
|
|
237
|
-
},
|
|
238
|
-
});
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
Events without `clientTxId` are treated as external writes.
|
|
218
|
+
Events without `clientTxId` are treated as external writes. The only thing you
|
|
219
|
+
add by hand is recording *outside* writes — changes made to your tables by a
|
|
220
|
+
cron job or admin tool that never went through Ablo. Append an `ablo_outbox` row
|
|
221
|
+
(with no `clientTxId`) for those in the same transaction as the change, and the
|
|
222
|
+
adapter's feed carries them to every connected screen.
|
|
242
223
|
|
|
243
224
|
## Production Checklist
|
|
244
225
|
|
|
@@ -246,14 +227,18 @@ Before using a customer-owned database in production:
|
|
|
246
227
|
|
|
247
228
|
- Keep `DATABASE_URL` in the customer app or backend environment.
|
|
248
229
|
- Use only the Data Source endpoint and `ABLO_API_KEY` as the customer-facing integration boundary.
|
|
249
|
-
-
|
|
250
|
-
|
|
251
|
-
-
|
|
252
|
-
-
|
|
253
|
-
|
|
254
|
-
- Dedupe outbox events by event `id`.
|
|
230
|
+
- Run the adapter migrations so `ablo_outbox` and `ablo_idempotency` exist
|
|
231
|
+
alongside your synced tables (`ablo migrate`).
|
|
232
|
+
- Set `export const runtime = 'nodejs'` on the route so it can reach the database.
|
|
233
|
+
- For writes that bypass Ablo (cron, admin tools), append an `ablo_outbox` row
|
|
234
|
+
(no `clientTxId`) in the same transaction as the change.
|
|
255
235
|
- Monitor last success, last error, retry count, event lag, and cursor.
|
|
256
236
|
|
|
237
|
+
The adapter already handles the rest — signature verification, the commit
|
|
238
|
+
transaction, `clientTxId` idempotency, returning canonical rows, the outbox
|
|
239
|
+
append per operation, and deduping the feed by event `id`. You don't write any of
|
|
240
|
+
that by hand.
|
|
241
|
+
|
|
257
242
|
Don't give Ablo your database URL for this integration — Ablo never connects to
|
|
258
243
|
your database directly. (Direct database access would be a separate product with
|
|
259
244
|
its own security model.)
|
|
@@ -7,7 +7,8 @@ Claims don't lock. If another writer holds the row, `claim` waits for them, re-r
|
|
|
7
7
|
```ts
|
|
8
8
|
import Ablo from '@abloatai/ablo';
|
|
9
9
|
import { defineSchema, model, z as schemaZ } from '@abloatai/ablo/schema';
|
|
10
|
-
import {
|
|
10
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
11
|
+
import { convertToModelMessages, streamText, tool, type UIMessage } from 'ai';
|
|
11
12
|
import { z } from 'zod';
|
|
12
13
|
|
|
13
14
|
const schema = defineSchema({
|
|
@@ -62,17 +63,22 @@ const updateReport = tool({
|
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
export async function POST(req: Request) {
|
|
65
|
-
|
|
66
|
+
// `useChat` posts UIMessage[]; the model is a server-bound provider instance,
|
|
67
|
+
// never read off the request body.
|
|
68
|
+
const { messages }: { messages: UIMessage[] } = await req.json();
|
|
66
69
|
|
|
67
70
|
return streamText({
|
|
68
|
-
model,
|
|
69
|
-
messages,
|
|
71
|
+
model: anthropic('claude-sonnet-4-6'),
|
|
72
|
+
messages: await convertToModelMessages(messages),
|
|
70
73
|
tools: { updateReport },
|
|
71
74
|
}).toUIMessageStreamResponse();
|
|
72
75
|
}
|
|
73
76
|
```
|
|
74
77
|
|
|
75
|
-
The model provider is interchangeable
|
|
78
|
+
The model provider is interchangeable — swap `anthropic(...)` for any
|
|
79
|
+
server-bound provider instance. What matters is that the route binds the model
|
|
80
|
+
on the server (never trusting one sent in the request body), converts the
|
|
81
|
+
incoming `UIMessage[]` with `convertToModelMessages`, and that the tool:
|
|
76
82
|
|
|
77
83
|
- reads the latest weather report with `retrieve` (a server read),
|
|
78
84
|
- claims the row — if someone else holds it, the claim waits for them, then re-reads,
|
|
@@ -46,7 +46,7 @@ export const schema = defineSchema({
|
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
```ts
|
|
49
|
-
// web/ablo.ts
|
|
49
|
+
// web/ablo.ts — SERVER-ONLY client (holds the sk_ key; never imported in the browser).
|
|
50
50
|
import Ablo from '@abloatai/ablo';
|
|
51
51
|
import { schema } from './ablo/schema';
|
|
52
52
|
|
|
@@ -56,18 +56,40 @@ export const ablo = Ablo({
|
|
|
56
56
|
});
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
Mount the React provider near the app root
|
|
60
|
-
|
|
59
|
+
Mount the React provider near the app root. Build the browser client first —
|
|
60
|
+
with an `authEndpoint` so it mints a short-lived session token instead of
|
|
61
|
+
carrying the secret key — then pass it to the provider via `client`.
|
|
61
62
|
|
|
62
63
|
```tsx
|
|
63
64
|
// web/app/providers.tsx
|
|
64
65
|
'use client';
|
|
65
66
|
|
|
67
|
+
import Ablo from '@abloatai/ablo';
|
|
66
68
|
import { AbloProvider } from '@abloatai/ablo/react';
|
|
67
69
|
import { schema } from '@/ablo/schema';
|
|
68
70
|
|
|
71
|
+
// Browser client: no secret key — `authEndpoint` mints the session token
|
|
72
|
+
// server-side (see the session route below).
|
|
73
|
+
const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
|
|
74
|
+
|
|
69
75
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
70
|
-
return <AbloProvider
|
|
76
|
+
return <AbloProvider client={ablo}>{children}</AbloProvider>;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The session route mints with the server client that holds the `sk_` key — the
|
|
81
|
+
browser only ever sees the short-lived token:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// web/app/api/ablo-session/route.ts
|
|
85
|
+
import { ablo } from '@/ablo';
|
|
86
|
+
|
|
87
|
+
export const runtime = 'nodejs';
|
|
88
|
+
|
|
89
|
+
export async function POST() {
|
|
90
|
+
const userId = await currentUserId(); // your auth
|
|
91
|
+
const { token } = await ablo.sessions.create({ user: { id: userId } });
|
|
92
|
+
return Response.json({ token });
|
|
71
93
|
}
|
|
72
94
|
```
|
|
73
95
|
|
package/docs/examples/nextjs.md
CHANGED
|
@@ -36,9 +36,10 @@ import { ablo } from '@/lib/ablo';
|
|
|
36
36
|
|
|
37
37
|
export default async function ReportPage({
|
|
38
38
|
params,
|
|
39
|
-
}: { params: { id: string } }) {
|
|
39
|
+
}: { params: Promise<{ id: string }> }) {
|
|
40
|
+
const { id } = await params;
|
|
40
41
|
await ablo.ready();
|
|
41
|
-
const report = await ablo.weatherReports.retrieve({ id
|
|
42
|
+
const report = await ablo.weatherReports.retrieve({ id });
|
|
42
43
|
if (!report) return null;
|
|
43
44
|
|
|
44
45
|
return <ReportEditor report={report} />;
|
|
@@ -46,26 +46,53 @@ export const schema = defineSchema(
|
|
|
46
46
|
## 2. Dispatch — narrow the agent to the deck it's working on
|
|
47
47
|
|
|
48
48
|
An agent can never reach more than the user who triggered it — that's the upper
|
|
49
|
-
limit. From there you narrow it to a single deck
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
limit. From there you narrow it to a single deck by minting the agent's session
|
|
50
|
+
against **just that deck's sync group**. You build the group from the **model
|
|
51
|
+
kind and id** with the typed `syncGroup` helper — `syncGroup('deck', deckId)`,
|
|
52
|
+
never a hand-assembled `deck:<id>` string — where `'deck'` is the kind declared
|
|
53
|
+
by the `decks` model's `scope`.
|
|
54
|
+
|
|
55
|
+
Mint the scoped session on your backend (it holds the `sk_` key; the browser
|
|
56
|
+
never does), then hand the short-lived token to the browser client:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// server — mints a scoped agent session for one deck
|
|
60
|
+
import Ablo from '@abloatai/ablo';
|
|
61
|
+
import { syncGroup } from '@abloatai/ablo/schema';
|
|
62
|
+
import { schema } from './schema';
|
|
63
|
+
|
|
64
|
+
const server = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
65
|
+
|
|
66
|
+
export async function mintDeckAgentSession(deckId: string, agentId: string) {
|
|
67
|
+
const { token } = await server.sessions.create({
|
|
68
|
+
agent: { id: agentId },
|
|
69
|
+
can: { slides: ['read', 'update'] }, // operation allowlist for this run
|
|
70
|
+
syncGroups: [syncGroup('deck', deckId)], // narrowed to just this deck
|
|
71
|
+
});
|
|
72
|
+
return token;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
52
75
|
|
|
53
76
|
```tsx
|
|
77
|
+
// client — the browser client carries only the scoped token.
|
|
78
|
+
import Ablo from '@abloatai/ablo';
|
|
54
79
|
import { AbloProvider } from '@abloatai/ablo/react';
|
|
80
|
+
import { schema } from './schema';
|
|
81
|
+
|
|
82
|
+
const ablo = Ablo({
|
|
83
|
+
schema,
|
|
84
|
+
getToken: async () => mintDeckAgentSession(deckId, agentId),
|
|
85
|
+
});
|
|
55
86
|
|
|
56
87
|
// The agent run is mounted on behalf of its triggering user.
|
|
57
|
-
<AbloProvider
|
|
58
|
-
schema={schema}
|
|
59
|
-
userId={triggeringUser.id} // ceiling: can't exceed this user's reach
|
|
60
|
-
scope={{ decks: deckId }} // floor: narrowed to just this deck → deck:<deckId>
|
|
61
|
-
>
|
|
88
|
+
<AbloProvider client={ablo} userId={triggeringUser.id}>
|
|
62
89
|
{children}
|
|
63
90
|
</AbloProvider>
|
|
64
91
|
```
|
|
65
92
|
|
|
66
|
-
`
|
|
67
|
-
|
|
68
|
-
never reach a deck its triggering user couldn't.
|
|
93
|
+
`syncGroups` requests, it never grants: at connect the server intersects the
|
|
94
|
+
groups the session asks for with the groups the identity is actually allowed,
|
|
95
|
+
so the agent can never reach a deck its triggering user couldn't.
|
|
69
96
|
|
|
70
97
|
## 3. Write — it fans out to everyone on that deck
|
|
71
98
|
|