@abloatai/ablo 0.11.1 → 0.11.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/CHANGELOG.md +34 -0
- package/README.md +10 -2
- package/dist/Model.d.ts +39 -0
- package/dist/Model.js +68 -0
- package/dist/auth/credentialPolicy.d.ts +145 -0
- package/dist/auth/credentialPolicy.js +130 -0
- package/dist/cli.cjs +39 -6
- package/dist/client/Ablo.d.ts +39 -88
- package/dist/client/Ablo.js +38 -98
- package/dist/client/ApiClient.d.ts +10 -1
- package/dist/client/ApiClient.js +19 -11
- package/dist/client/auth.d.ts +12 -5
- package/dist/client/auth.js +2 -1
- package/dist/client/createModelProxy.d.ts +49 -10
- package/dist/client/createModelProxy.js +6 -0
- package/dist/client/httpClient.d.ts +17 -3
- package/dist/client/httpClient.js +1 -0
- package/dist/client/identity.js +134 -122
- package/dist/client/index.d.ts +1 -1
- package/dist/client/sessionMint.d.ts +15 -0
- package/dist/client/sessionMint.js +86 -0
- package/dist/errorCodes.d.ts +2 -0
- package/dist/errorCodes.js +2 -0
- package/dist/errors.d.ts +3 -2
- package/dist/errors.js +3 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -7
- package/dist/mutators/RecordingTransaction.js +14 -42
- package/dist/react/AbloProvider.d.ts +1 -6
- package/dist/react/AbloProvider.js +1 -5
- package/dist/react/context.d.ts +1 -31
- package/dist/react/context.js +2 -2
- package/dist/react/index.d.ts +0 -6
- package/dist/react/index.js +0 -7
- package/dist/react/useSyncStatus.d.ts +1 -1
- package/dist/realtime/index.d.ts +1 -1
- package/dist/schema/generate.js +1 -2
- package/dist/schema/schema.d.ts +13 -2
- package/dist/schema/schema.js +26 -0
- package/dist/surface.d.ts +29 -0
- package/dist/surface.js +60 -0
- package/dist/sync/ConnectionManager.d.ts +16 -5
- package/dist/sync/ConnectionManager.js +42 -7
- package/dist/transactions/TransactionQueue.d.ts +0 -11
- package/dist/transactions/TransactionQueue.js +12 -56
- package/dist/types/global.d.ts +3 -0
- package/dist/types/streams.d.ts +0 -22
- package/dist/utils/mobx-setup.js +1 -0
- package/docs/api-keys.md +49 -0
- package/docs/api.md +3 -2
- package/docs/client-behavior.md +1 -0
- package/docs/coordination.md +75 -21
- package/docs/examples/existing-python-backend.md +9 -5
- package/docs/examples/scoped-agent.md +1 -1
- package/docs/guarantees.md +4 -3
- package/docs/identity.md +89 -82
- package/docs/integration-guide.md +19 -10
- package/docs/migration.md +9 -2
- package/docs/quickstart.md +6 -2
- package/docs/react.md +3 -3
- package/docs/schema-contract.md +23 -5
- package/llms-full.txt +18 -16
- package/llms.txt +6 -6
- package/package.json +1 -1
- package/dist/api/index.d.ts +0 -10
- package/dist/api/index.js +0 -9
- package/dist/principal.d.ts +0 -44
- package/dist/principal.js +0 -49
- package/dist/react/SyncGroupProvider.d.ts +0 -19
- package/dist/react/SyncGroupProvider.js +0 -44
- package/dist/react/useClaim.d.ts +0 -29
- package/dist/react/useClaim.js +0 -42
- package/dist/react/usePresence.d.ts +0 -32
- package/dist/react/usePresence.js +0 -41
package/docs/identity.md
CHANGED
|
@@ -36,8 +36,8 @@ runnable place, so the concepts below have code to attach to.
|
|
|
36
36
|
## Declare it, end to end
|
|
37
37
|
|
|
38
38
|
The entire declaration surface is: `identityRoles` (who may see what), and on
|
|
39
|
-
each model `scope` / `parent` / `grants` (which group a row fans out on), plus
|
|
40
|
-
optional `
|
|
39
|
+
each model `scope` / `parent` / `grants` (which group a row fans out on), plus
|
|
40
|
+
optional `syncGroups` at session-mint time (narrowing). Read the three blocks first —
|
|
41
41
|
a human gets their `org` / `team` scope, an agent gets one `deck` — then the
|
|
42
42
|
sections after explain each.
|
|
43
43
|
|
|
@@ -84,18 +84,17 @@ export const schema = defineSchema(
|
|
|
84
84
|
|
|
85
85
|
```ts
|
|
86
86
|
// 3. an AGENT run inherits its user, narrowed to the entities in play.
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
// You narrow at SESSION-MINT time: your backend calls `sessions.create` with the
|
|
88
|
+
// agent's allowed `syncGroups`, built from each model's scope via the
|
|
89
|
+
// `syncGroup(kind, id)` helper — never a hand-built `deck:<id>` string. The agent's
|
|
90
|
+
// runtime then connects with the minted token.
|
|
91
|
+
const session = await server.sessions.create({
|
|
92
|
+
agent: { id: agentId },
|
|
93
|
+
can: { Deck: ['read', 'update'] },
|
|
94
|
+
syncGroups: [syncGroup('deck', deckId)], // floor: just the deck it's working on
|
|
94
95
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
{children}
|
|
98
|
-
</AbloProvider>;
|
|
96
|
+
// the agent runtime authenticates with the minted token
|
|
97
|
+
const ablo = Ablo({ schema, apiKey: session.token });
|
|
99
98
|
```
|
|
100
99
|
|
|
101
100
|
That's the whole surface. The rest of this doc is the *why* behind each line.
|
|
@@ -127,11 +126,12 @@ so you pass them in code when you start the run.
|
|
|
127
126
|
> **One line:** humans subscribe by who they are; agents subscribe by what
|
|
128
127
|
> they've been given.
|
|
129
128
|
|
|
130
|
-
That's why you never write per-user scope code, but you always
|
|
131
|
-
|
|
129
|
+
That's why you never write per-user scope code, but you always choose an agent's
|
|
130
|
+
groups at the dispatch site. A user's org/team/user don't change per request, so
|
|
132
131
|
their scope is a **rule the schema derives automatically**. An agent's reach
|
|
133
132
|
depends on *what it's working on*, which is only knowable at dispatch — so you
|
|
134
|
-
|
|
133
|
+
pass its `syncGroups` **when your backend mints the agent session**
|
|
134
|
+
(`sessions.create({ agent, can, syncGroups })`). The schema's
|
|
135
135
|
only job for entities is to declare *that* a model is
|
|
136
136
|
entity-scopable and *what its group is named* (`scope: 'deck'` → `deck:{id}`);
|
|
137
137
|
it never declares *which* entities a given agent gets. (A human can opt into the
|
|
@@ -305,8 +305,9 @@ server, never by the browser.**
|
|
|
305
305
|
|
|
306
306
|
The identity your server resolved is carried by the client you build and the
|
|
307
307
|
`userId` prop. In a Next.js app, resolve the user in a Server Component and pass
|
|
308
|
-
it down. Build the client once (the schema, `teamIds`, and
|
|
309
|
-
live here), then hand
|
|
308
|
+
it down. Build the client once (the schema, `teamIds`, and the `apiKey` resolver
|
|
309
|
+
live here; entity narrowing rides the minted session's `syncGroups`), then hand
|
|
310
|
+
it to the provider:
|
|
310
311
|
|
|
311
312
|
```ts
|
|
312
313
|
// lib/ablo.ts
|
|
@@ -318,7 +319,10 @@ import { schema } from '@/ablo/schema';
|
|
|
318
319
|
export function makeAblo(user: { teamIds: string[] }) {
|
|
319
320
|
return Ablo({
|
|
320
321
|
schema,
|
|
321
|
-
|
|
322
|
+
// The browser holds no secret — the `apiKey` resolver fetches the
|
|
323
|
+
// short-lived session token your `/api/ablo-session` route minted, and the
|
|
324
|
+
// client keeps it fresh before expiry.
|
|
325
|
+
apiKey: () => fetch('/api/ablo-session').then((r) => r.text()),
|
|
322
326
|
teamIds: user.teamIds,
|
|
323
327
|
});
|
|
324
328
|
}
|
|
@@ -354,7 +358,7 @@ What carries identity — and just as importantly, what does *not* set the bound
|
|
|
354
358
|
| ------------ | ------------------------------------------------------------------------------------------------ |
|
|
355
359
|
| `userId` prop | App-level participant id, used for app-owned fields and read by your `identityRole` `source`. **Not** the security boundary — the server enforces scope from the authenticated request. |
|
|
356
360
|
| `teamIds` (on the client) | Team ids expanded into team sync groups via your `identityRoles`. |
|
|
357
|
-
| `
|
|
361
|
+
| `syncGroups` (at session mint) | Optional. **Narrows** a minted session's subscription to a subset of what auth already allows — it can never widen it. Passed to `sessions.create({ user \| agent, syncGroups })`; build entries with `syncGroup(kind, id)`. Use it to scope an agent (or a focused page's session) to one entity, e.g. `[syncGroup('deck', 'abc123')]`. |
|
|
358
362
|
|
|
359
363
|
Because the server is the boundary, a client that changes `userId` to another
|
|
360
364
|
user's id does not gain their data — the server resolves and enforces the real
|
|
@@ -398,20 +402,20 @@ subset of what its user could see:
|
|
|
398
402
|
|
|
399
403
|
```ts
|
|
400
404
|
// agent run triggered by `user`, working on one document + one deck.
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
scope: { documents: documentId, decks: deckId },
|
|
405
|
+
// Your backend mints the agent session narrowed to just the entities in play
|
|
406
|
+
// (the floor). Build each group from the model's scope with `syncGroup(kind, id)`.
|
|
407
|
+
const session = await server.sessions.create({
|
|
408
|
+
agent: { id: agentId },
|
|
409
|
+
can: { Document: ['read', 'update'], Deck: ['read', 'update'] },
|
|
410
|
+
syncGroups: [syncGroup('document', documentId), syncGroup('deck', deckId)],
|
|
408
411
|
});
|
|
409
|
-
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
+
// identity (the ceiling) is inherited from the triggering user via your
|
|
413
|
+
// session-mint logic; the agent runtime connects with the minted token.
|
|
414
|
+
const ablo = Ablo({ schema, apiKey: session.token });
|
|
412
415
|
```
|
|
413
416
|
|
|
414
|
-
As the run touches more entities
|
|
417
|
+
As the run touches more entities, claim or read them and the client auto-enrolls
|
|
418
|
+
in their entity groups — its set **accretes** to cover them; it never
|
|
415
419
|
widens past the user's ceiling, and it carries no standing access to entities it
|
|
416
420
|
isn't working on. The `identityRoles` need no agent-specific entry: the agent
|
|
417
421
|
carries the triggering user's `userId`, so the same `user:{id}` role that scopes
|
|
@@ -444,52 +448,55 @@ runs the [Coordinating long agent work](../README.md#coordinating-long-agent-wor
|
|
|
444
448
|
`claim` loop is, to the scoping layer, that same participant — scoped to the row
|
|
445
449
|
it claimed.
|
|
446
450
|
|
|
447
|
-
## Narrowing to specific entities
|
|
448
|
-
|
|
449
|
-
A human gets their full membership automatically (`identityRoles`).
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
`
|
|
454
|
-
|
|
455
|
-
`
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const ablo = Ablo({
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
>
|
|
484
|
-
> (
|
|
485
|
-
>
|
|
486
|
-
>
|
|
487
|
-
|
|
488
|
-
>
|
|
489
|
-
>
|
|
490
|
-
|
|
491
|
-
>
|
|
492
|
-
>
|
|
451
|
+
## Narrowing to specific entities
|
|
452
|
+
|
|
453
|
+
A human gets their full membership automatically (`identityRoles`). There are
|
|
454
|
+
three ways to narrow a participant to specific entities — a page on one deck, or
|
|
455
|
+
an agent pointed at the entities it's working on. You **never hand-write**
|
|
456
|
+
`deck:<id>`; build groups from the model's `scope` (Half 2) with the typed
|
|
457
|
+
`syncGroup(kind, id)` helper from `@abloatai/ablo/schema`.
|
|
458
|
+
|
|
459
|
+
1. **At session mint — `syncGroups`.** When your backend mints a session, pass the
|
|
460
|
+
exact groups it may subscribe to. This is the floor for a delegated agent (and
|
|
461
|
+
the way to scope a focused page's session):
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
// an agent working across two decks and a document
|
|
465
|
+
const session = await server.sessions.create({
|
|
466
|
+
agent: { id: agentId },
|
|
467
|
+
can: { Deck: ['read', 'update'], Document: ['read'] },
|
|
468
|
+
syncGroups: [
|
|
469
|
+
syncGroup('deck', deckA),
|
|
470
|
+
syncGroup('deck', deckB),
|
|
471
|
+
syncGroup('document', docId),
|
|
472
|
+
],
|
|
473
|
+
});
|
|
474
|
+
const ablo = Ablo({ schema, apiKey: session.token });
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
2. **Automatically, on read or claim.** Reading a row (`retrieve`/`get`/
|
|
478
|
+
`claim.state`) auto-enrolls the client in that row's entity group
|
|
479
|
+
(**read-interest**), and `claim`-ing it pins a **write-intent** subscription.
|
|
480
|
+
So an agent's reachable set **accretes** as it works — no extra subscribe call.
|
|
481
|
+
|
|
482
|
+
3. **Explicitly, for presence — `watch`.** To hold presence on a known set of rows
|
|
483
|
+
and react to peers, use the WebSocket-only `ablo.<model>.watch(ids, { ttl })`
|
|
484
|
+
(it returns a participant handle with `.peers`). See
|
|
485
|
+
[Coordination](./coordination.md).
|
|
486
|
+
|
|
487
|
+
> **`scope` is the schema model option, not a client setting.** `scope: 'deck'`
|
|
488
|
+
> in `model(...)` declares a scope root ([Half 2](#half-2--per-model-scope-row--group)) —
|
|
489
|
+
> it names the group (`deck:<id>`) that the mechanisms above then subscribe to.
|
|
490
|
+
> There is no `Ablo({ scope })` constructor option. The lifecycle filter on
|
|
491
|
+
> [`list()`](./api.md#model-methods) is a separate axis named **`state`**
|
|
492
|
+
> (`'live' | 'archived' | 'all'`, GitHub's open/closed/all), precisely so it
|
|
493
|
+
> doesn't share the word.
|
|
494
|
+
|
|
495
|
+
> **Requested groups never grant.** At connect, the server intersects the session's
|
|
496
|
+
> `syncGroups` with what the identity is actually allowed (`requested ∩ allowed`).
|
|
497
|
+
> So `syncGroups` only ever *narrows* within a participant's ceiling — an agent
|
|
498
|
+
> can't reach a deck its capability doesn't already permit, no matter what it
|
|
499
|
+
> passes. Smaller bootstrap, less fan-out, same server-enforced boundary.
|
|
493
500
|
|
|
494
501
|
## How this compares — and the best practices it follows
|
|
495
502
|
|
|
@@ -512,7 +519,7 @@ how to reason about it.
|
|
|
512
519
|
the room/shape and the server signs off, as in
|
|
513
520
|
[Pusher's channel authorization endpoint](https://pusher.com/docs/channels/server_api/authorizing-users/),
|
|
514
521
|
[ElectricSQL **gatekeeper auth**](https://github.com/electric-sql/electric/blob/main/examples/gatekeeper-auth/README.md),
|
|
515
|
-
and Liveblocks **access tokens**. Ablo's
|
|
522
|
+
and Liveblocks **access tokens**. Ablo's session-mint `syncGroups` is the
|
|
516
523
|
*narrowing* half of this — but it can only ever shrink the server-derived set,
|
|
517
524
|
never grow it.
|
|
518
525
|
|
|
@@ -529,10 +536,10 @@ The best practices Ablo inherits from that lineage:
|
|
|
529
536
|
2. **Trusted vs untrusted claims is the whole security argument.** PowerSync draws
|
|
530
537
|
the line precisely: [token parameters are trusted and usable for access
|
|
531
538
|
control; client parameters are not](https://docs.powersync.com/usage/sync-rules/advanced-topics/client-parameters).
|
|
532
|
-
In Ablo terms, the identity your server vouches for
|
|
533
|
-
|
|
534
|
-
client input* — convenient for app-owned fields
|
|
535
|
-
boundary. This is why changing `userId` in the browser grants nothing.
|
|
539
|
+
In Ablo terms, the identity your server vouches for — and the session's
|
|
540
|
+
`syncGroups`, minted server-side — are the *trusted* claims that set scope; the
|
|
541
|
+
`userId` prop is *untrusted client input* — convenient for app-owned fields, but
|
|
542
|
+
never the boundary. This is why changing `userId` in the browser grants nothing.
|
|
536
543
|
|
|
537
544
|
3. **Scope by a hierarchical naming convention, declared once.** Ablo's `kind:id`
|
|
538
545
|
group naming (`org:…` / `team:…` from `identityRoles`, `deck:…` from a model's
|
|
@@ -44,13 +44,18 @@ objects by hand.
|
|
|
44
44
|
|
|
45
45
|
## Your Database
|
|
46
46
|
|
|
47
|
-
Every schema model is backed by **your own database**.
|
|
48
|
-
|
|
49
|
-
Postgres. The SDK call shape is the same everywhere.
|
|
47
|
+
Every schema model is backed by **your own database**. The SDK call shape is the
|
|
48
|
+
same everywhere.
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
`
|
|
53
|
-
|
|
50
|
+
In this guide — an app that already owns its backend and database — keep
|
|
51
|
+
`DATABASE_URL` inside your app and expose a signed Data Source endpoint: Ablo
|
|
52
|
+
coordinates each write and your app commits it to your Postgres. Do not pass
|
|
53
|
+
`databaseUrl` to `Ablo(...)` here; application and agent code use `ABLO_API_KEY`.
|
|
54
|
+
|
|
55
|
+
If instead you want Ablo to connect to your Postgres directly, pass `databaseUrl`
|
|
56
|
+
(a live, server-only option) to `Ablo(...)`. That and the sandbox-only `apiKey`
|
|
57
|
+
shape are the other two start states — [Connect Your Database](./data-sources.md)
|
|
58
|
+
is the single source of truth for all three.
|
|
54
59
|
|
|
55
60
|
## Test With Sandboxes
|
|
56
61
|
|
|
@@ -94,12 +99,13 @@ import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
|
94
99
|
export const schema = defineSchema(
|
|
95
100
|
{
|
|
96
101
|
weatherReports: model({
|
|
97
|
-
id
|
|
102
|
+
// Reserved fields (id, createdAt, updatedAt, organizationId, createdBy)
|
|
103
|
+
// are SDK-provided automatically — never declare them. Declare only your
|
|
104
|
+
// own fields.
|
|
98
105
|
projectId: z.string(),
|
|
99
106
|
location: z.string(),
|
|
100
107
|
status: z.enum(['pending', 'ready']),
|
|
101
108
|
assigneeId: z.string().nullable(),
|
|
102
|
-
updatedAt: z.string(),
|
|
103
109
|
}),
|
|
104
110
|
},
|
|
105
111
|
{
|
|
@@ -169,7 +175,7 @@ export const ablo = Ablo({
|
|
|
169
175
|
Browser apps should use the React provider or a scoped session token, not a
|
|
170
176
|
server API key in the bundle. Build the client first, then hand it to the
|
|
171
177
|
provider — `AbloProvider` takes `{ client, userId?, onError?, fallback? }`, and
|
|
172
|
-
nothing else (`schema`, `teamIds`,
|
|
178
|
+
nothing else (`schema`, `teamIds`, and `apiKey` all live on the
|
|
173
179
|
client now).
|
|
174
180
|
|
|
175
181
|
```tsx
|
|
@@ -179,7 +185,10 @@ import { schema } from '@/ablo/schema';
|
|
|
179
185
|
|
|
180
186
|
// The browser never holds the API key. The client mints a short-lived token
|
|
181
187
|
// from your session route (see below) and refreshes it before expiry.
|
|
182
|
-
export const ablo = Ablo({
|
|
188
|
+
export const ablo = Ablo({
|
|
189
|
+
schema,
|
|
190
|
+
apiKey: () => fetch('/api/ablo-session').then((r) => r.text()),
|
|
191
|
+
});
|
|
183
192
|
```
|
|
184
193
|
|
|
185
194
|
```tsx
|
package/docs/migration.md
CHANGED
|
@@ -113,6 +113,11 @@ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: 'http'
|
|
|
113
113
|
await ablo.tasks.update({ id, data: { status: 'done' } });
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
> **Minting still needs the stateful client.** `sessions.create(...)` is not on
|
|
117
|
+
> the `transport: 'http'` surface. Keep a default-transport `server` client for
|
|
118
|
+
> minting short-lived credentials (see the 0.9.2 example below), and use the
|
|
119
|
+
> http client for the per-request reads and writes.
|
|
120
|
+
|
|
116
121
|
---
|
|
117
122
|
|
|
118
123
|
## 0.9.2 — `turn` / agent-`tasks` removed; `intents` deprecated
|
|
@@ -135,8 +140,10 @@ helper, and the agent/task type family (`Agent`, `AgentOptions`,
|
|
|
135
140
|
```diff
|
|
136
141
|
- const turn = await engine.beginTurn();
|
|
137
142
|
- await Ablo({ apiKey }).agent(agentId, opts).run(prompt, handler);
|
|
138
|
-
+ // Mint a scoped credential
|
|
139
|
-
+
|
|
143
|
+
+ // Mint a scoped credential from a stateful (default-transport) server client —
|
|
144
|
+
+ // sessions.create lives on the stateful client, not on transport: 'http'.
|
|
145
|
+
+ const server = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
146
|
+
+ const { token } = await server.sessions.create({ agent: { id: agentId } });
|
|
140
147
|
+ const agent = Ablo({ schema, apiKey: token });
|
|
141
148
|
+ await using claim = await agent.tasks.claim({ id });
|
|
142
149
|
+ await agent.tasks.update({ id, data: { status: 'done' }, wait: 'confirmed' });
|
package/docs/quickstart.md
CHANGED
|
@@ -57,6 +57,9 @@ export const schema = defineSchema({
|
|
|
57
57
|
});
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
**Reserved fields** — `id`, `createdAt`, `updatedAt`, `organizationId`, and
|
|
61
|
+
`createdBy` are provided by the SDK automatically. Don't declare them in your
|
|
62
|
+
`model(...)` fields; declare only your own.
|
|
60
63
|
|
|
61
64
|
The schema is registered once (init scaffolds `ablo/register.ts` for you), and
|
|
62
65
|
every type is one parameter away — no `typeof schema` re-stating, anywhere:
|
|
@@ -210,8 +213,9 @@ and you write the usual way with `ablo.<model>.update({ id, data })`.
|
|
|
210
213
|
Claims don't lock. If another writer holds the row, `claim` waits for them,
|
|
211
214
|
re-reads the fresh row, then hands it to you — so two writers serialize instead
|
|
212
215
|
of clobbering. Normal reads still work while the claim is held. If a server read
|
|
213
|
-
should not return a row while someone else is mid-edit, pass `ifClaimed: '
|
|
214
|
-
to
|
|
216
|
+
should not return a row while someone else is mid-edit, pass `ifClaimed: 'fail'`
|
|
217
|
+
to error out instead. Reads never block on a claim — to wait for a row to free
|
|
218
|
+
up, `claim({ id })` it (the claim queues fairly behind the holder).
|
|
215
219
|
Call `handle.release()` when your work is done.
|
|
216
220
|
|
|
217
221
|
```ts
|
package/docs/react.md
CHANGED
|
@@ -31,7 +31,7 @@ import { schema } from '@/ablo/schema';
|
|
|
31
31
|
// from your own server route (see Identity below).
|
|
32
32
|
export const ablo = Ablo({
|
|
33
33
|
schema,
|
|
34
|
-
|
|
34
|
+
apiKey: () => fetch('/api/ablo-session').then((r) => r.text()),
|
|
35
35
|
});
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -65,13 +65,13 @@ export function Providers({
|
|
|
65
65
|
|
|
66
66
|
| Prop | Default | Purpose |
|
|
67
67
|
| ----------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
|
|
68
|
-
| `client` | — | **Required.** The `Ablo({ schema,
|
|
68
|
+
| `client` | — | **Required.** The `Ablo({ schema, apiKey })` instance. It carries the schema and connection config. |
|
|
69
69
|
| `userId` | resolved from auth | App participant id for app-owned fields and your `identityRoles`. Not the security boundary. |
|
|
70
70
|
| `fallback` | neutral spinner | Rendered during the *first* bootstrap only. Pass a branded skeleton, `null`, or `'passthrough'`. |
|
|
71
71
|
| `onError` | — | Engine / WebSocket / bootstrap errors. Wire to Sentry / Datadog. |
|
|
72
72
|
|
|
73
73
|
Everything that used to be a provider prop — `schema`, `url`, `apiKey`,
|
|
74
|
-
`teamIds`, `syncGroups
|
|
74
|
+
`teamIds`, `syncGroups`, `persistence`, `bootstrapMode` — now lives on
|
|
75
75
|
the `Ablo({ ... })` client you build before mounting the provider. Where the
|
|
76
76
|
identity comes from, and why the API key never reaches the browser, is the whole
|
|
77
77
|
of [Identity & Sync Groups](./identity.md) — read that if it isn't obvious how
|
package/docs/schema-contract.md
CHANGED
|
@@ -49,6 +49,20 @@ The model key (`weatherReports`) becomes the client namespace
|
|
|
49
49
|
contract. You should not create a parallel string-keyed write path for the same
|
|
50
50
|
data.
|
|
51
51
|
|
|
52
|
+
### Reserved fields
|
|
53
|
+
|
|
54
|
+
The SDK provides these on every row automatically — do **not** declare them in
|
|
55
|
+
your `model(...)` fields:
|
|
56
|
+
|
|
57
|
+
- `id`
|
|
58
|
+
- `createdAt`
|
|
59
|
+
- `updatedAt`
|
|
60
|
+
- `organizationId`
|
|
61
|
+
- `createdBy`
|
|
62
|
+
|
|
63
|
+
Declare only your own fields; the reserved ones are still present on the row and
|
|
64
|
+
readable, you just don't author them.
|
|
65
|
+
|
|
52
66
|
## Reads and writes
|
|
53
67
|
|
|
54
68
|
Use async reads when the row may not be local:
|
|
@@ -88,12 +102,16 @@ the fresh row. Reads stay open; only acting on the row serializes.
|
|
|
88
102
|
|
|
89
103
|
## Storage boundary
|
|
90
104
|
|
|
91
|
-
Every schema model is backed by your own database
|
|
92
|
-
|
|
105
|
+
Every schema model is backed by your own database. There are three start states,
|
|
106
|
+
all covered in [Connect Your Database](./data-sources.md) (the single source of
|
|
107
|
+
truth): the sandbox (`apiKey` only, no database), a direct connection string
|
|
108
|
+
(`databaseUrl` passed to `Ablo(...)`, a live, server-only option), or a signed
|
|
109
|
+
Data Source endpoint where your app keeps the database credential and commits
|
|
110
|
+
each write itself.
|
|
93
111
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
API key.
|
|
112
|
+
If your database stays canonical behind a Data Source endpoint, do not pass
|
|
113
|
+
`databaseUrl` to `Ablo(...)` — trusted runtimes use `ABLO_API_KEY`. Browser code
|
|
114
|
+
goes through `<AbloProvider>` or a scoped session route, never a raw API key.
|
|
97
115
|
|
|
98
116
|
## Rules of thumb
|
|
99
117
|
|
package/llms-full.txt
CHANGED
|
@@ -39,9 +39,9 @@ Use `snapshot(...)` and `readAt` when an agent write depends on state it already
|
|
|
39
39
|
read. `onStale: 'reject'` prevents lost updates by rejecting if the target
|
|
40
40
|
changed after the snapshot.
|
|
41
41
|
|
|
42
|
-
Claims are live coordination signals, not database locks.
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
Claims are live coordination signals, not database locks. Reads never block on a
|
|
43
|
+
claim — to wait for a row to free up, `claim({ id })` it and the claim queues
|
|
44
|
+
fairly behind the current holder.
|
|
45
45
|
|
|
46
46
|
Agent run bookkeeping is internal. Most users should not create run ledger
|
|
47
47
|
records or scoped access credentials manually.
|
|
@@ -307,7 +307,10 @@ common agent path.
|
|
|
307
307
|
|
|
308
308
|
## Model
|
|
309
309
|
|
|
310
|
-
|
|
310
|
+
The read shape depends on whether the client has a local graph:
|
|
311
|
+
|
|
312
|
+
- The stateful `ablo.<model>.retrieve({ id })` returns the BARE row (`T | undefined`), and `list({ where })` returns `T[]`. Read coordination metadata (state watermark, active claims) separately via `ablo.snapshot(...)` and `ablo.<model>.claim.state({ id })`.
|
|
313
|
+
- The stateless HTTP / `.model(name)` `retrieve({ id })` returns a `ModelRead<T>` envelope because it has no local graph to carry the watermark; `list({ where })` still returns `T[]`.
|
|
311
314
|
|
|
312
315
|
```ts
|
|
313
316
|
type ModelRead<T> = {
|
|
@@ -317,32 +320,31 @@ type ModelRead<T> = {
|
|
|
317
320
|
};
|
|
318
321
|
```
|
|
319
322
|
|
|
320
|
-
`stamp` is the state watermark. Pass it to writes as `readAt
|
|
323
|
+
`stamp` is the state watermark. Pass it to writes as `readAt` (from `ablo.snapshot(...)` on the stateful client, or from the envelope on the HTTP client).
|
|
321
324
|
|
|
322
325
|
`claims` lists active work on the target. Reads are allowed while another participant is working. The caller decides whether to return, fail, or wait.
|
|
323
326
|
|
|
324
327
|
## Claimed Behavior
|
|
325
328
|
|
|
326
|
-
Claimed behavior is explicit:
|
|
329
|
+
Claimed behavior is explicit, and there are only two policies:
|
|
327
330
|
|
|
328
|
-
- `ifClaimed: 'return'` returns the current claims with the read.
|
|
331
|
+
- `ifClaimed: 'return'` (the default) returns the row plus the current claims with the read.
|
|
329
332
|
- `ifClaimed: 'fail'` throws `AbloClaimedError`.
|
|
330
|
-
- `ifClaimed: 'wait'` waits for matching claims to clear.
|
|
331
|
-
|
|
332
|
-
Schema clients use the realtime claim stream for waits.
|
|
333
333
|
|
|
334
|
-
|
|
334
|
+
Reads never block on a claim. To wait for a row to free up, `claim({ id })` it —
|
|
335
|
+
the claim queues fairly behind the current holder and is granted when the row
|
|
336
|
+
frees. Use `ifClaimed: 'fail'` when you'd rather refuse to read a claimed row.
|
|
335
337
|
|
|
336
338
|
```ts
|
|
339
|
+
// Read a claimed row without blocking, surfacing who holds it:
|
|
337
340
|
await api.model('reports').retrieve({
|
|
338
341
|
id: 'report_stockholm',
|
|
339
|
-
ifClaimed: '
|
|
340
|
-
claimedPollInterval: 1_000,
|
|
341
|
-
claimedTimeout: 30_000,
|
|
342
|
+
ifClaimed: 'return',
|
|
342
343
|
});
|
|
343
|
-
```
|
|
344
344
|
|
|
345
|
-
|
|
345
|
+
// Or wait for it to free up by queueing your own claim:
|
|
346
|
+
await api.model('reports').claim({ id: 'report_stockholm' });
|
|
347
|
+
```
|
|
346
348
|
|
|
347
349
|
## Write
|
|
348
350
|
|
package/llms.txt
CHANGED
|
@@ -99,13 +99,13 @@ coordination until the app reports it through Data Source events.
|
|
|
99
99
|
## Claimed Behavior
|
|
100
100
|
|
|
101
101
|
Reads never silently block. Schema reads stay open while a row is claimed.
|
|
102
|
-
Server reads through `ablo.model(name)` can pass `ifClaimed: 'return'`
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
Server reads through `ablo.model(name)` can pass `ifClaimed: 'return'` (the
|
|
103
|
+
default — returns the row plus active claim metadata) or `ifClaimed: 'fail'`
|
|
104
|
+
to throw `AbloClaimedError`.
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Use `
|
|
106
|
+
Reads never block on a claim. To wait for a row to free up, `claim({ id })` it —
|
|
107
|
+
the claim queues fairly behind the current holder and is granted when the row
|
|
108
|
+
frees. Use `ifClaimed: 'fail'` when you'd rather refuse to read a claimed row.
|
|
109
109
|
|
|
110
110
|
## Guarantees
|
|
111
111
|
|
package/package.json
CHANGED
package/dist/api/index.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal compatibility entrypoint for the stateless hosted protocol client.
|
|
3
|
-
*
|
|
4
|
-
* Use this build for serverless functions, scripts, and backends that want
|
|
5
|
-
* model reads/writes and commits over HTTP without the realtime sync runtime.
|
|
6
|
-
*/
|
|
7
|
-
export { createProtocolClient, createProtocolClient as Ablo, type AbloApi, type AbloApiClientOptions, type AbloApiClaims, type Capability, type CapabilityCreateOptions, type CapabilityParticipantKind, type CapabilityRecord, type CapabilityResource, type CapabilityRevocation, type CapabilityScope, } from '../client/ApiClient.js';
|
|
8
|
-
export type { CommitCreateOptions, CommitOperationInput, CommitReceipt, CommitWait, ClaimCreateOptions, ClaimHandle, ClaimWaitOptions, ClaimedOptions, IfClaimedPolicy, ModelClient, ModelClaim, ModelMutationOptions, ModelReadOptions, ModelRead, ModelTarget, } from '../client/Ablo.js';
|
|
9
|
-
import { createProtocolClient } from '../client/ApiClient.js';
|
|
10
|
-
export default createProtocolClient;
|
package/dist/api/index.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal compatibility entrypoint for the stateless hosted protocol client.
|
|
3
|
-
*
|
|
4
|
-
* Use this build for serverless functions, scripts, and backends that want
|
|
5
|
-
* model reads/writes and commits over HTTP without the realtime sync runtime.
|
|
6
|
-
*/
|
|
7
|
-
export { createProtocolClient, createProtocolClient as Ablo, } from '../client/ApiClient.js';
|
|
8
|
-
import { createProtocolClient } from '../client/ApiClient.js';
|
|
9
|
-
export default createProtocolClient;
|
package/dist/principal.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Principal constructors — a thin typed façade over the raw
|
|
3
|
-
* `SessionRef` / `AgentRef` shapes so call sites don't have to memorize
|
|
4
|
-
* the discriminated-union tags.
|
|
5
|
-
*
|
|
6
|
-
* ```ts
|
|
7
|
-
* import Ablo, { session } from '@abloatai/ablo';
|
|
8
|
-
*
|
|
9
|
-
* const ablo = Ablo({ schema, apiKey });
|
|
10
|
-
* const participant = await ablo.participants.join({
|
|
11
|
-
* type: 'Matter',
|
|
12
|
-
* id: 'deal-1',
|
|
13
|
-
* });
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* Browser-human flows use `session(...)`. Agent-spawn-agent flows use
|
|
17
|
-
* `agent(...)`, but those rarely appear in customer code because the
|
|
18
|
-
* participant layer handles attenuation.
|
|
19
|
-
*
|
|
20
|
-
* These are pure — no I/O, no hidden state. If the shape ever grows a
|
|
21
|
-
* required field (say, a scope hint for the restricted key), the helper
|
|
22
|
-
* is the one place to flag migrations.
|
|
23
|
-
*/
|
|
24
|
-
import type { AgentRef, SessionRef } from './types/streams.js';
|
|
25
|
-
/**
|
|
26
|
-
* Build a `SessionRef` from the identifiers your auth system already
|
|
27
|
-
* holds. Typical inputs: the Better Auth session id, the user id, and
|
|
28
|
-
* the organization the session is scoped to.
|
|
29
|
-
*/
|
|
30
|
-
export declare function session(params: {
|
|
31
|
-
id: string;
|
|
32
|
-
userId: string;
|
|
33
|
-
organizationId: string;
|
|
34
|
-
}): SessionRef;
|
|
35
|
-
/**
|
|
36
|
-
* Build an `AgentRef` from an agent id + the capability token that
|
|
37
|
-
* authenticates it. Rare in application code — the common path is
|
|
38
|
-
* `participant.join(child)` where the parent's token is attenuated
|
|
39
|
-
* automatically.
|
|
40
|
-
*/
|
|
41
|
-
export declare function agent(params: {
|
|
42
|
-
id: string;
|
|
43
|
-
capabilityToken: string;
|
|
44
|
-
}): AgentRef;
|