@abloatai/ablo 0.52.0 → 0.54.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 +254 -0
- package/docs/api-keys.md +3 -3
- package/docs/api.md +31 -2
- package/docs/client-behavior.md +1 -1
- package/docs/customer-organizations.md +3 -3
- package/docs/identity.md +1 -1
- package/docs/sessions.md +2 -2
- package/examples/agent-turn.ts +2 -2
- package/examples/lease-outlives-the-machine.ts +66 -0
- package/examples/tsconfig.json +4 -10
- package/llms.txt +28 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,259 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.54.0
|
|
4
|
+
|
|
5
|
+
### A scoped session is scoped everywhere it is read
|
|
6
|
+
|
|
7
|
+
Seven surfaces each answered "which sync groups may this request see", and two of
|
|
8
|
+
them consulted only `effectiveSyncGroups` before falling back to an
|
|
9
|
+
organization-wide anchor. `syncGroups` is the only field an `ek_` session key
|
|
10
|
+
populates, so a session scoped to one workspace was correctly narrowed on five
|
|
11
|
+
surfaces and read organization-wide on the other two. Nothing failed while they
|
|
12
|
+
disagreed, because each surface's tests pinned that surface to itself.
|
|
13
|
+
|
|
14
|
+
One module now owns the precedence, and a plane states its difference as an
|
|
15
|
+
argument rather than as another copy of the rule. A declared set that is empty
|
|
16
|
+
means nothing rather than everything, so a session minted with no groups closes
|
|
17
|
+
instead of widening.
|
|
18
|
+
|
|
19
|
+
### A change reaches the clients watching it, whatever the column is called
|
|
20
|
+
|
|
21
|
+
Deltas were written in two key shapes. The commit path wrote declared schema
|
|
22
|
+
field names; the replication echo wrote the customer's physical column names
|
|
23
|
+
undecoded. Neither reader reconciled them, because the client applies a delta
|
|
24
|
+
onto the model verbatim.
|
|
25
|
+
|
|
26
|
+
For a source whose columns are renamed, by `.from(...)` or simply by being
|
|
27
|
+
snake_case, every change reached subscribers keyed wrong, and the lookup that
|
|
28
|
+
stamps a scope-root group found nothing on a physical row. Such a delta kept only
|
|
29
|
+
its organization group, so a client joined to `workspace:<id>` was never sent a
|
|
30
|
+
change it was watching: the write landed, and nothing was announced to anyone
|
|
31
|
+
listening. Rows are renamed once now, where they enter, and one spelling holds
|
|
32
|
+
below that seam.
|
|
33
|
+
|
|
34
|
+
### Reordering a claim queue takes effect
|
|
35
|
+
|
|
36
|
+
A reorder took effect for nobody. The route addressed the frame to an
|
|
37
|
+
organization group, which entity-scoped fan-out removes, so the frame was built
|
|
38
|
+
and then dropped as having no audience. A queue change now goes to the waiters in
|
|
39
|
+
that line, each of which recorded what it listens on when it enqueued.
|
|
40
|
+
|
|
41
|
+
### A commit costs a fixed number of round trips
|
|
42
|
+
|
|
43
|
+
A direct write paid three round trips to the customer's database plus one per
|
|
44
|
+
row. Sharing a region that is a few milliseconds, but across continents it
|
|
45
|
+
dominated the wait: an engine in `eu-north-1` against a database in `us-east-2`
|
|
46
|
+
measured about four seconds per confirmed write, most of it in trips nobody had
|
|
47
|
+
counted.
|
|
48
|
+
|
|
49
|
+
Three changes remove trips without altering what the database sees. The session
|
|
50
|
+
bundle is one statement over parallel name and value arrays rather than eleven
|
|
51
|
+
settings awaited in turn, and is still transaction-scoped. The ledger completion
|
|
52
|
+
and the replication marker travel as one data-modifying statement, which Postgres
|
|
53
|
+
runs to completion whether or not the primary query reads it. And a direct commit
|
|
54
|
+
dispatches its operations before awaiting any of them, so the driver pipelines
|
|
55
|
+
them.
|
|
56
|
+
|
|
57
|
+
Ordering is unchanged: Postgres still runs those operations in order on the
|
|
58
|
+
connection, so a later one still sees an earlier one's write, two writes to the
|
|
59
|
+
same row stay well-defined last-write-wins, and each operation keeps its own
|
|
60
|
+
error so a failure still names itself.
|
|
61
|
+
|
|
62
|
+
### Engine-reserved groups have a constructor
|
|
63
|
+
|
|
64
|
+
`identityAnchor` builds the sync groups the engine reserves, so the `kind:id`
|
|
65
|
+
convention has one home instead of being spelled inline:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { identityAnchor } from '@abloatai/ablo/schema';
|
|
69
|
+
|
|
70
|
+
identityAnchor('org', organizationId);
|
|
71
|
+
identityAnchor('user', participantId);
|
|
72
|
+
identityAnchor('project', projectId);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`IDENTITY_ANCHOR_KINDS` and `IdentityAnchorKind` are exported alongside it.
|
|
76
|
+
Schema-declared roles continue to extend this vocabulary per application; these
|
|
77
|
+
three are the kinds the engine reserves.
|
|
78
|
+
|
|
79
|
+
### The cross-organization scope is `organization:act-as`
|
|
80
|
+
|
|
81
|
+
The scope authorizing a secret key to mint a session into another organization is
|
|
82
|
+
now `organization:act-as`, and it names what it grants rather than the mechanism
|
|
83
|
+
it was first attached to. Keys already carrying `ephemeral:mint-any-org` keep
|
|
84
|
+
working, because the old spelling resolves to the new one.
|
|
85
|
+
|
|
86
|
+
### An outbox event carries declared field names
|
|
87
|
+
|
|
88
|
+
A hand-written `events` handler must key its `data` by the model's declared
|
|
89
|
+
schema fields rather than by the table's columns. A field named `reviewStatus`
|
|
90
|
+
arrives as `reviewStatus` even when it reads from a `review_status` column:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// the model declares reviewStatus from a review_status column
|
|
94
|
+
data: { id: row.id, reviewStatus: row.review_status },
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Ablo's own adapters rename the row before writing the outbox, so a source built
|
|
98
|
+
on one of them is already in this shape. Ablo reads that spelling and never falls
|
|
99
|
+
back to the physical one: two namespaces that can collide have no safe merge, and
|
|
100
|
+
a key read as the wrong field would route a change into another scope root.
|
|
101
|
+
|
|
102
|
+
### CLI: report what got in your way
|
|
103
|
+
|
|
104
|
+
`ablo feedback` is the channel for the two things no counter can carry, because
|
|
105
|
+
neither is a sentence: the doc that was missing, and the thing that worked but
|
|
106
|
+
was hard.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
ablo feedback docs "no example of paging a filtered list" --yes
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`<kind>` is `bug`, `docs`, `feature`, or `friction`. Add `--detail <text>` for
|
|
113
|
+
the long version, where `-` reads stdin, and `--command` or `--error-code` to
|
|
114
|
+
pre-group the report from what you just saw. `--yes` sends without confirming
|
|
115
|
+
and `--json` returns a machine-readable receipt, so a non-interactive caller
|
|
116
|
+
needs no terminal.
|
|
117
|
+
|
|
118
|
+
It is never automatic. Nothing sends unless the command is run, and nothing
|
|
119
|
+
rides the telemetry queue, so turning telemetry off does not also turn off bug
|
|
120
|
+
reporting, and leaving it on does not start sending prose. The text is redacted
|
|
121
|
+
before it leaves, by the same rule error observations already pass through, and
|
|
122
|
+
on a terminal you see the redacted version before it is sent. Nothing is read
|
|
123
|
+
from your repository, and there is no flag to attach a file.
|
|
124
|
+
|
|
125
|
+
### Removed
|
|
126
|
+
|
|
127
|
+
`normalizeAbloHostedBaseUrl` is removed, as 0.53.0 announced. Use
|
|
128
|
+
`normalizeAbloBaseUrl`, which the old name has resolved to since then.
|
|
129
|
+
|
|
130
|
+
`CapabilityExchangeResponse` is announced for removal in 0.55.0. Use
|
|
131
|
+
`CapabilityMintResponse`; both already resolve to the same contract.
|
|
132
|
+
|
|
133
|
+
## 0.53.0
|
|
134
|
+
|
|
135
|
+
### A collection read says where the collection ends
|
|
136
|
+
|
|
137
|
+
`list` returns a page. The result is still an array, so it maps, spreads, and
|
|
138
|
+
iterates exactly as before, and it now carries `hasMore` and `nextCursor` beside
|
|
139
|
+
the rows. Pass `nextCursor` back as `cursor`, keeping `where` and `orderBy` the
|
|
140
|
+
same, to walk the rest:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
let cursor: string | null = null;
|
|
144
|
+
const open = [];
|
|
145
|
+
do {
|
|
146
|
+
const page = await ablo.weatherReports.list({
|
|
147
|
+
where: { status: ['draft', 'review'] },
|
|
148
|
+
orderBy: { createdAt: 'asc' },
|
|
149
|
+
limit: 100,
|
|
150
|
+
...(cursor ? { cursor } : {}),
|
|
151
|
+
});
|
|
152
|
+
open.push(...page);
|
|
153
|
+
cursor = page.hasMore ? page.nextCursor : null;
|
|
154
|
+
} while (cursor);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A list read has always been a page: the server applies a default size and caps
|
|
158
|
+
the largest one. Until now that page state was dropped on arrival, so a read
|
|
159
|
+
that returned twenty of five hundred matching rows looked exactly like a
|
|
160
|
+
complete one. Check `hasMore` before treating a result as the whole set.
|
|
161
|
+
|
|
162
|
+
The live client keeps a local graph and loads a working set rather than pages, so
|
|
163
|
+
it rejects `cursor` instead of returning the first page again. Narrow the
|
|
164
|
+
`where`, or construct the client with `transport: 'http'` to page. On the live
|
|
165
|
+
client `hasMore` reports whether a `limit` cut the working set short, and
|
|
166
|
+
`nextCursor` is `null`.
|
|
167
|
+
|
|
168
|
+
`GET /v1/projects` returns the same list envelope as every other collection,
|
|
169
|
+
with `has_more` and `next_cursor` beside `data`.
|
|
170
|
+
|
|
171
|
+
### The page cursor is called `cursor`
|
|
172
|
+
|
|
173
|
+
The parameter that resumes a collection is `cursor`, in the SDK and on every
|
|
174
|
+
HTTP collection route. It was `starting_after`, a spelling whose established
|
|
175
|
+
meaning elsewhere is a row id, while this value has always been an opaque token
|
|
176
|
+
tied to the sort it was issued for. A caller who read the familiar name and
|
|
177
|
+
passed a row id was refused, so the name promised something it never did.
|
|
178
|
+
|
|
179
|
+
`starting_after` is still accepted on the wire and is removed in a later
|
|
180
|
+
release. Requests that send it keep working; new code should send `cursor`.
|
|
181
|
+
Sending both uses `cursor`. The MCP `list_records` tool and the OpenAPI
|
|
182
|
+
description take `cursor`, and the spec marks the old name deprecated.
|
|
183
|
+
|
|
184
|
+
### A filter reaches the server intact
|
|
185
|
+
|
|
186
|
+
`where` accepts operators as well as equality. An array value is an `IN`, and
|
|
187
|
+
tuple form spells the rest out:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const storms = await ablo.weatherReports.list({
|
|
191
|
+
where: [
|
|
192
|
+
['title', 'ILIKE', '%storm%'],
|
|
193
|
+
['createdAt', '>=', cutoff],
|
|
194
|
+
['status', 'IN', ['draft', 'review']],
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Clauses combine with AND. For OR, run two reads and union the results.
|
|
200
|
+
|
|
201
|
+
On the stateless client (`transport: 'http'`) an `IN` filter and every
|
|
202
|
+
tuple-form clause were previously discarded before the request left, and the
|
|
203
|
+
read came back unfiltered. An agent or worker that filtered a collection over
|
|
204
|
+
HTTP was reading more rows than it asked for, with nothing to indicate it. Every
|
|
205
|
+
transport now encodes a filter the same way.
|
|
206
|
+
|
|
207
|
+
A filter on a boolean field could also match the opposite rows rather than fail,
|
|
208
|
+
when its value arrived as the database's own text spelling. Boolean values are
|
|
209
|
+
coerced before binding, and read back the same way.
|
|
210
|
+
|
|
211
|
+
### A number field reads back as a number
|
|
212
|
+
|
|
213
|
+
A field declared as a number arrives as one whatever integer width its column
|
|
214
|
+
uses. A wide column previously came back as a decimal string while its narrower
|
|
215
|
+
neighbour came back as a number, so the type a caller received depended on a
|
|
216
|
+
database detail the schema had already settled.
|
|
217
|
+
|
|
218
|
+
A stored value beyond the range a JavaScript number represents exactly now fails
|
|
219
|
+
with `column_value_out_of_range` rather than arriving quietly rounded. Declare
|
|
220
|
+
such a field as text to read those values digit for digit.
|
|
221
|
+
|
|
222
|
+
### A reconnect cannot roll back a confirmed write
|
|
223
|
+
|
|
224
|
+
Each row in the live client records the log position it reflects. A bootstrap or
|
|
225
|
+
an on-demand read from an earlier position is left unapplied, so a snapshot that
|
|
226
|
+
arrives late no longer overwrites a row the client already knows to be newer.
|
|
227
|
+
The ordered change stream continues to carry every other writer's edits. A
|
|
228
|
+
plugin receives that position as `syncId` on `AppliedChange`.
|
|
229
|
+
|
|
230
|
+
### The base URL is checked where the credential travels
|
|
231
|
+
|
|
232
|
+
`baseURL` accepts an HTTPS origin, preserving a path prefix for a deployment
|
|
233
|
+
mounted under one, and plain HTTP for localhost. A URL that embeds its own
|
|
234
|
+
credentials, or carries a query or a fragment, is refused when the client is
|
|
235
|
+
constructed rather than failing later as an opaque request error. Every request
|
|
236
|
+
attaches the resolved key against this origin, so the rule lives beside the
|
|
237
|
+
option rather than in each application that sets it.
|
|
238
|
+
|
|
239
|
+
`normalizeAbloHostedBaseUrl` is now `normalizeAbloBaseUrl`. The old name
|
|
240
|
+
resolves to the same function and is removed in 0.54.0.
|
|
241
|
+
|
|
242
|
+
### Two error codes added
|
|
243
|
+
|
|
244
|
+
`organization_disabled` is returned when an operator has disabled an
|
|
245
|
+
organization, and `query_relation_expansion_too_large` when a requested relation
|
|
246
|
+
expansion exceeds the nested-row budget. The error contract version is
|
|
247
|
+
`2026-08-15`.
|
|
248
|
+
|
|
249
|
+
### CLI
|
|
250
|
+
|
|
251
|
+
Where a command sends a management key is resolved and checked in one place: an
|
|
252
|
+
explicit `--url` on the commands that take one, then `ABLO_API_URL`, then the
|
|
253
|
+
hosted default. A host given without a scheme becomes absolute, and a
|
|
254
|
+
destination that would put the key on the wire in clear, or one carrying its own
|
|
255
|
+
credentials, is refused before the request is made.
|
|
256
|
+
|
|
3
257
|
## 0.52.0
|
|
4
258
|
|
|
5
259
|
### Models carry only `id`
|
package/docs/api-keys.md
CHANGED
|
@@ -217,7 +217,7 @@ restricted to exactly those grants:
|
|
|
217
217
|
- `project:manage` — list, create, and rename projects.
|
|
218
218
|
- `branch:manage` — list, create, and delete child branches and mint their
|
|
219
219
|
temporary credentials.
|
|
220
|
-
- `
|
|
220
|
+
- `organization:act-as` — cross-organization authority to mint a short-lived
|
|
221
221
|
user session into a customer organization. It follows the Stripe Connect shape:
|
|
222
222
|
the request names the customer organization, but the resulting session is
|
|
223
223
|
still bounded by its `can` grant and expiry. A key restricted to this scope
|
|
@@ -234,7 +234,7 @@ manage siblings or gain root authority.
|
|
|
234
234
|
|
|
235
235
|
### Cross-organization mint keys
|
|
236
236
|
|
|
237
|
-
Most applications do not need `
|
|
237
|
+
Most applications do not need `organization:act-as`: their backend key mints
|
|
238
238
|
users into its own organization. A multi-organization backend needs it only
|
|
239
239
|
when each customer is a separate Ablo organization and one trusted service
|
|
240
240
|
mints sessions for all of them.
|
|
@@ -242,7 +242,7 @@ mints sessions for all of them.
|
|
|
242
242
|
Treat that key as a dedicated minting credential:
|
|
243
243
|
|
|
244
244
|
- keep it in a server-side secret manager, never a browser or repository;
|
|
245
|
-
- grant only `
|
|
245
|
+
- grant only `organization:act-as`, with no data or schema scopes;
|
|
246
246
|
- mint short-lived sessions with the smallest typed `can` grant;
|
|
247
247
|
- rotate it on a schedule and revoke it immediately after suspected exposure;
|
|
248
248
|
- log the target `organizationId`, minted session id, and request id for audit.
|
package/docs/api.md
CHANGED
|
@@ -67,7 +67,7 @@ fallback removed — nothing to await, so they return a value.
|
|
|
67
67
|
| Method | Returns | Use when |
|
|
68
68
|
|---|---|---|
|
|
69
69
|
| `get({ id })` | `Promise<T \| undefined>` | You need one row, hydrating from local store and server. |
|
|
70
|
-
| `list({ where })` | `Promise<T
|
|
70
|
+
| `list({ where })` | `Promise<ModelList<T>>` | You need to hydrate a collection from local store and server. |
|
|
71
71
|
| `local.get(id)` | `T \| undefined` | You want a synchronous snapshot of one local row. |
|
|
72
72
|
| `local.list(options?)` | `T[]` | You want a synchronous snapshot of a local collection. |
|
|
73
73
|
| `local.count(options?)` | `number` | You want a synchronous count of local rows. |
|
|
@@ -79,6 +79,33 @@ fallback removed — nothing to await, so they return a value.
|
|
|
79
79
|
through the server. The `local` reads work off the rows a session has already
|
|
80
80
|
synced, so a cheap re-read needs no round-trip.
|
|
81
81
|
|
|
82
|
+
### Paging a collection
|
|
83
|
+
|
|
84
|
+
`list` returns a page. The result is an array, so it maps and iterates as
|
|
85
|
+
before, and it carries `hasMore` and `nextCursor` alongside the rows:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
let cursor: string | null = null;
|
|
89
|
+
const open = [];
|
|
90
|
+
do {
|
|
91
|
+
const page = await ablo.weatherReports.list({
|
|
92
|
+
where: { status: ['draft', 'review'] },
|
|
93
|
+
orderBy: { createdAt: 'asc' },
|
|
94
|
+
limit: 100,
|
|
95
|
+
...(cursor ? { cursor } : {}),
|
|
96
|
+
});
|
|
97
|
+
open.push(...page);
|
|
98
|
+
cursor = page.hasMore ? page.nextCursor : null;
|
|
99
|
+
} while (cursor);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Keep `where` and `orderBy` the same across pages: the cursor encodes the sort
|
|
103
|
+
position it was issued for, and a read that changes either starts a new walk.
|
|
104
|
+
|
|
105
|
+
`where` accepts operators as well as equality, and both travel to the server:
|
|
106
|
+
`{ status: ['draft', 'review'] }` is an `IN`, and tuple form spells the rest
|
|
107
|
+
out, as in `[['title', 'ILIKE', '%storm%'], ['createdAt', '>=', cutoff]]`.
|
|
108
|
+
|
|
82
109
|
## Protected Writes
|
|
83
110
|
|
|
84
111
|
Use `snapshot` when a write should reject if the row changed mid-flight:
|
|
@@ -193,7 +220,9 @@ receipt; the typed SDK turns single-model writes into their application result
|
|
|
193
220
|
(the created or updated row, or nothing for delete). A rejected write carries an
|
|
194
221
|
error `code` (e.g. `stale_context`, `intent_conflict`) to act on.
|
|
195
222
|
`GET /api/v1/models/{model}` is cursor-paginated (`limit`, `order`, `order_by`,
|
|
196
|
-
`
|
|
223
|
+
`cursor`) and returns `{ data, has_more, next_cursor }`. The `starting_after`
|
|
224
|
+
spelling this parameter used through 0.52.0 is still honoured, and is removed in
|
|
225
|
+
a later release.
|
|
197
226
|
|
|
198
227
|
`POST /api/v1/commits` remains the path for **atomic multi-op** writes (several
|
|
199
228
|
operations across rows/models that must commit together) — the per-model routes
|
package/docs/client-behavior.md
CHANGED
|
@@ -31,7 +31,7 @@ Common options:
|
|
|
31
31
|
|---|---|
|
|
32
32
|
| `schema` | Required for typed model clients. |
|
|
33
33
|
| `apiKey` | Bearer credential for trusted server runtimes. Defaults to `ABLO_API_KEY` when available. |
|
|
34
|
-
| `baseURL` | Override the hosted sync endpoint for staging or private deployments. |
|
|
34
|
+
| `baseURL` | Override the hosted sync endpoint for staging or private deployments. An HTTPS origin, optionally with a path prefix; plain HTTP is accepted for localhost. Your key travels here, so a URL carrying its own credentials, a query, or a fragment is refused at construction. |
|
|
35
35
|
| `persistence` | `memory` by default. Use `indexeddb` for a durable browser cache that survives reloads. |
|
|
36
36
|
| `durableWrites` | Optional crash recovery for unacknowledged agent/worker writes. Independent of the default memory cache; accepts `{ store, namespace? }`. |
|
|
37
37
|
| `transport` | `'websocket'` (default) is the live, stateful client: a persistent socket, a local synced pool, and `onChange` subscriptions. `'http'` returns the **stateless** client for server-side actors (agents, workers, serverless): the same `ablo.<model>` read/write/claim surface, but each call is one HTTP round-trip with no socket. Under `'http'` the return type narrows to `AbloHttpClient`, so stateful-only methods (the `local` reads, `onChange`, `join`) are compile errors rather than runtime gaps. |
|
|
@@ -26,7 +26,7 @@ user session names that customer organization.
|
|
|
26
26
|
|
|
27
27
|
- An owning project containing the schema every customer uses.
|
|
28
28
|
- The schema pushed to that project's production root.
|
|
29
|
-
- A server-side `sk_` carrying only `
|
|
29
|
+
- A server-side `sk_` carrying only `organization:act-as`.
|
|
30
30
|
- A customer `organizationId` resolved from your authenticated application
|
|
31
31
|
membership, never accepted unchecked from the browser.
|
|
32
32
|
- A model-by-model `can` grant for the UI being opened.
|
|
@@ -159,7 +159,7 @@ through the control-plane process you use for provisioning.
|
|
|
159
159
|
## Security checklist
|
|
160
160
|
|
|
161
161
|
- Store the cross-organization key only in the backend secret manager.
|
|
162
|
-
- Give it only `
|
|
162
|
+
- Give it only `organization:act-as`; do not combine minting with schema or
|
|
163
163
|
data authority.
|
|
164
164
|
- Resolve `organizationId` from authenticated membership server-side.
|
|
165
165
|
- Keep `can` to the smallest model/verb set the UI needs.
|
|
@@ -175,7 +175,7 @@ through the control-plane process you use for provisioning.
|
|
|
175
175
|
### The mint is forbidden
|
|
176
176
|
|
|
177
177
|
The presenting credential must be a secret `sk_` with
|
|
178
|
-
`
|
|
178
|
+
`organization:act-as`. A normal project key can mint users into its own
|
|
179
179
|
organization but cannot name another one. Run `npx ablo whoami --json` in the
|
|
180
180
|
backend environment to confirm which project and branch the configured key
|
|
181
181
|
actually belongs to; the command never prints the full secret.
|
package/docs/identity.md
CHANGED
|
@@ -238,7 +238,7 @@ shared schema only *describes* the shape; the data plane stays the customer's an
|
|
|
238
238
|
can't cross-leak. `schemaProject: { organizationId, projectId }` remains
|
|
239
239
|
available as an explicit override for migrations or advanced routing. Omit
|
|
240
240
|
`organizationId` for the single-organization default above. Requires a dedicated
|
|
241
|
-
`sk_` with `
|
|
241
|
+
`sk_` with `organization:act-as`; see
|
|
242
242
|
[Customer Organizations](./customer-organizations.md).
|
|
243
243
|
|
|
244
244
|
## The two halves of scoping
|
package/docs/sessions.md
CHANGED
|
@@ -154,7 +154,7 @@ for the actor.
|
|
|
154
154
|
|---|---|---|
|
|
155
155
|
| `user` / `agent` | both | The actor. `id` becomes the token's `participantId`. Pass exactly one. |
|
|
156
156
|
| `can` | both | Required non-empty per-model operation allowlist, typed off the schema. |
|
|
157
|
-
| `organizationId` | user | Mint into a customer organization instead of the key's own. Requires `
|
|
157
|
+
| `organizationId` | user | Mint into a customer organization instead of the key's own. Requires `organization:act-as`. |
|
|
158
158
|
| `schemaProject` | user | Override the schema project for a cross-org mint. Usually omitted because the owning key's project is the default. |
|
|
159
159
|
| `syncGroups` | both | Narrow the session below its default scope. Omit to inherit. |
|
|
160
160
|
| `ttlSeconds` | both | Lifetime in seconds. Defaults to `900` (15m). |
|
|
@@ -286,7 +286,7 @@ plane (connection + row-level isolation) stays the customer's. A shared schema
|
|
|
286
286
|
can't leak data across orgs.
|
|
287
287
|
|
|
288
288
|
<Note>
|
|
289
|
-
This requires a dedicated `sk_` carrying the `
|
|
289
|
+
This requires a dedicated `sk_` carrying the `organization:act-as` scope —
|
|
290
290
|
only a trusted cross-organization key can mint a session into another org. Omit
|
|
291
291
|
`organizationId` and you get the default above: one project, one schema, all
|
|
292
292
|
your users in the key's own organization.
|
package/examples/agent-turn.ts
CHANGED
|
@@ -30,8 +30,8 @@ try {
|
|
|
30
30
|
reads: [record],
|
|
31
31
|
idempotencyKey: commitId,
|
|
32
32
|
});
|
|
33
|
-
const
|
|
34
|
-
console.log({ identity: ablo.identity, commit
|
|
33
|
+
const commit = await ablo.commits.get({ id: commitId });
|
|
34
|
+
console.log({ identity: ablo.identity, commit });
|
|
35
35
|
} finally {
|
|
36
36
|
await ablo.dispose();
|
|
37
37
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lease outlives the process that took it.
|
|
3
|
+
*
|
|
4
|
+
* Run in two terminals against your own project. The `holder` takes a claim
|
|
5
|
+
* and is killed without releasing it, exactly as a sandbox that is torn down
|
|
6
|
+
* mid-turn would be. The `successor`, already queued, is granted the claim when
|
|
7
|
+
* the lease lapses and reads the row as it stands then.
|
|
8
|
+
*
|
|
9
|
+
* Terminal 1: ABLO_API_KEY=sk_... JOB_ID=job_... npx tsx examples/lease-outlives-the-machine.ts holder
|
|
10
|
+
* Terminal 2: ABLO_API_KEY=sk_... JOB_ID=job_... npx tsx examples/lease-outlives-the-machine.ts successor
|
|
11
|
+
*
|
|
12
|
+
* Start the successor first, then the holder, so the queue is populated before
|
|
13
|
+
* the lease lapses.
|
|
14
|
+
*/
|
|
15
|
+
import { Ablo } from '@abloatai/ablo';
|
|
16
|
+
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
17
|
+
|
|
18
|
+
const schema = defineSchema({
|
|
19
|
+
jobs: model({
|
|
20
|
+
prompt: z.string(),
|
|
21
|
+
status: z.enum(['pending', 'complete']),
|
|
22
|
+
answer: z.string().optional(),
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const role = process.argv[2];
|
|
27
|
+
if (role !== 'holder' && role !== 'successor') {
|
|
28
|
+
throw new Error('Pass "holder" or "successor" as the first argument');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const jobId = process.env.JOB_ID;
|
|
32
|
+
if (!jobId) throw new Error('JOB_ID is required');
|
|
33
|
+
|
|
34
|
+
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
35
|
+
await ablo.ready();
|
|
36
|
+
|
|
37
|
+
if (role === 'holder') {
|
|
38
|
+
// A short TTL and no heartbeat: this process takes the lease and then stops
|
|
39
|
+
// proving it is alive, which is what a machine that disappears looks like
|
|
40
|
+
// from the server's side.
|
|
41
|
+
const claim = await ablo.jobs.claim({
|
|
42
|
+
id: jobId,
|
|
43
|
+
description: 'drafting the summary',
|
|
44
|
+
ttl: '10s',
|
|
45
|
+
});
|
|
46
|
+
console.log('holder: lease taken, status is', claim.data.status);
|
|
47
|
+
console.log('holder: exiting without releasing it');
|
|
48
|
+
// Deliberately skip release and skip dispose. `process.exit` runs no
|
|
49
|
+
// cleanup, so the server never hears from this participant again.
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('successor: queueing behind whoever holds the lease');
|
|
54
|
+
const started = Date.now();
|
|
55
|
+
await using claim = await ablo.jobs.claim({
|
|
56
|
+
id: jobId,
|
|
57
|
+
description: 'taking over the draft',
|
|
58
|
+
ttl: '30s',
|
|
59
|
+
heartbeat: { every: '10s' },
|
|
60
|
+
});
|
|
61
|
+
console.log(`successor: granted after ${Math.round((Date.now() - started) / 1000)}s`);
|
|
62
|
+
console.log('successor: read the row as it stands now —', {
|
|
63
|
+
status: claim.data.status,
|
|
64
|
+
answer: claim.data.answer,
|
|
65
|
+
});
|
|
66
|
+
await ablo.dispose();
|
package/examples/tsconfig.json
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
+
"extends": "../tsconfig.json",
|
|
2
3
|
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"lib": ["ES2022", "DOM"],
|
|
7
|
-
"strict": true,
|
|
8
4
|
"noEmit": true,
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"types": ["node"]
|
|
5
|
+
"rootDir": "..",
|
|
6
|
+
"lib": ["ES2022", "ESNext.Disposable"]
|
|
13
7
|
},
|
|
14
8
|
"include": ["**/*.ts"],
|
|
15
|
-
"exclude": ["node_modules"]
|
|
9
|
+
"exclude": ["node_modules", "dist"]
|
|
16
10
|
}
|
package/llms.txt
CHANGED
|
@@ -85,6 +85,17 @@ second verb to learn — `local.` is the only difference. The query reads accept
|
|
|
85
85
|
and `state`; state defaults to `'live'`, with `'archived'` and `'all'` to include
|
|
86
86
|
retired rows.
|
|
87
87
|
|
|
88
|
+
`where` takes operators, not only equality: an array value is an `IN`
|
|
89
|
+
(`{ status: ['draft', 'review'] }`), and tuple form spells the rest out
|
|
90
|
+
(`[['title', 'ILIKE', '%storm%'], ['createdAt', '>=', cutoff]]`). Clauses combine
|
|
91
|
+
with AND; for OR, run two reads and union them.
|
|
92
|
+
|
|
93
|
+
`list` returns a page, not always the whole collection. The result is an array,
|
|
94
|
+
so it maps and iterates as usual, and it carries `hasMore` and `nextCursor`
|
|
95
|
+
beside the rows. Pass `nextCursor` back as `cursor`, keeping `where` and
|
|
96
|
+
`orderBy` the same, to walk the rest. Check `hasMore` before treating a result
|
|
97
|
+
as complete.
|
|
98
|
+
|
|
88
99
|
Workers import the same app schema and select `transport: 'http'`. The transport
|
|
89
100
|
changes; the typed `ablo.<model>` contract does not. There is no public
|
|
90
101
|
schema-less or string-keyed model client.
|
|
@@ -232,4 +243,20 @@ Do not teach `/api`, `/agent`, `/core`, `/realtime`, or internal subpaths. (`/so
|
|
|
232
243
|
|
|
233
244
|
- `npx ablo docs` lists every documentation page; `npx ablo docs <page>` prints one as markdown. These pages ship INSIDE the installed package, so they describe the version in `node_modules` and need no network. Prefer them over a docs URL whenever the project pins a version: a website always describes the newest release, so on an older pin it will hand you a call your package does not have (`retrieve`/`list` replaced `get`/`getAll`/`getCount` in 0.35.0). One-shot, safe to run unattended.
|
|
234
245
|
|
|
235
|
-
|
|
246
|
+
## Documentation
|
|
247
|
+
|
|
248
|
+
Canonical docs to read before integrating, in this order. Read each with `npx ablo docs <page>` when the project pins a version; the links describe the newest release.
|
|
249
|
+
|
|
250
|
+
- [Quickstart](https://docs.abloatai.com/quickstart)
|
|
251
|
+
- [Branch-first development](https://docs.abloatai.com/branch-development)
|
|
252
|
+
- [Schema Contract](https://docs.abloatai.com/schema-contract)
|
|
253
|
+
- [Integration Guide](https://docs.abloatai.com/integration-guide)
|
|
254
|
+
- [Deployment](https://docs.abloatai.com/deployment)
|
|
255
|
+
- [Guarantees](https://docs.abloatai.com/guarantees)
|
|
256
|
+
- [Client Behavior](https://docs.abloatai.com/client-behavior)
|
|
257
|
+
- [Connect Your Database](https://docs.abloatai.com/data-sources)
|
|
258
|
+
- [API](https://docs.abloatai.com/api)
|
|
259
|
+
- Examples: [Existing Python Backend](https://docs.abloatai.com/examples/existing-python-backend), [AI SDK Tool](https://docs.abloatai.com/examples/ai-sdk-tool), [Server Agent](https://docs.abloatai.com/examples/server-agent)
|
|
260
|
+
- [Upgrade Guide](https://docs.abloatai.com/migration): when upgrading an existing integration; every breaking change, what to change, and which version introduced it.
|
|
261
|
+
- [Session Settings](https://docs.abloatai.com/session-settings): when the customer's database has row-level-security policies; the identity context Ablo sets before every write, and how to map it to the setting names those policies read.
|
|
262
|
+
- [Every page, one line each](https://docs.abloatai.com/llms.txt), or [the full docs as one file](https://docs.abloatai.com/llms-full.txt).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/ablo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"description": "The public Ablo SDK for coordinated reads, commits, claims, observation, and reactive applications.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"prepack": "npm run build && node scripts/strip-source-condition.mjs",
|
|
113
113
|
"postpack": "node scripts/restore-source-condition.mjs",
|
|
114
114
|
"pack:check": "node scripts/pack-check.mjs",
|
|
115
|
-
"typecheck": "tsc --noEmit && tsc -p typetests/tsconfig.json",
|
|
115
|
+
"typecheck": "tsc --noEmit && tsc -p typetests/tsconfig.json && tsc -p examples/tsconfig.json",
|
|
116
116
|
"test": "vitest run",
|
|
117
117
|
"generate:errors": "tsx scripts/generate-error-docs.mts",
|
|
118
118
|
"lint:errors": "tsx scripts/check-error-docs.mts",
|
|
@@ -137,8 +137,8 @@
|
|
|
137
137
|
"directory": "packages/ablo"
|
|
138
138
|
},
|
|
139
139
|
"dependencies": {
|
|
140
|
-
"@abloatai/humans": "^0.
|
|
141
|
-
"@abloatai/transaction": "^0.
|
|
140
|
+
"@abloatai/humans": "^0.54.0",
|
|
141
|
+
"@abloatai/transaction": "^0.54.0",
|
|
142
142
|
"zod": "^4.4.3"
|
|
143
143
|
},
|
|
144
144
|
"peerDependencies": {
|