@abloatai/ablo 0.59.1 → 0.60.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 +68 -0
- package/README.md +1 -1
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +2 -0
- package/dist/sessions.js.map +1 -0
- package/docs/agent-messaging.md +3 -2
- package/docs/agents.md +84 -35
- package/docs/api-keys.md +18 -12
- package/docs/client-behavior.md +6 -4
- package/docs/coordination.md +6 -3
- package/docs/customer-organizations.md +8 -19
- package/docs/deployment.md +7 -5
- package/docs/examples/existing-python-backend.md +15 -18
- package/docs/examples/nextjs.md +33 -66
- package/docs/examples/scoped-agent.md +4 -3
- package/docs/examples/server-agent.md +4 -3
- package/docs/groups.md +11 -12
- package/docs/identity.md +33 -29
- package/docs/integration-guide.md +12 -18
- package/docs/options.md +83 -30
- package/docs/react.md +8 -52
- package/docs/security.md +1 -1
- package/docs/sessions.md +97 -69
- package/docs/transports.md +124 -0
- package/examples/README.md +7 -0
- package/examples/terminal-showcase/index.ts +219 -0
- package/examples/terminal-showcase/schema.ts +12 -0
- package/llms.txt +9 -9
- package/package.json +10 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.60.0
|
|
4
|
+
|
|
5
|
+
### Sessions are the connection boundary for people and agents
|
|
6
|
+
|
|
7
|
+
`Sessions({ schema, apiKey })` is now the dedicated session issuer. Backends create scoped
|
|
8
|
+
agent sessions with `sessions.create({ agent, can, groups })` and expose browser
|
|
9
|
+
sessions with `sessions.handler({ authenticate, grant })`. Both return the same
|
|
10
|
+
short-lived session contract, and both are supplied to clients through
|
|
11
|
+
`Ablo({ schema, session })`:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const workerAccess = {
|
|
15
|
+
records: ['read', 'update'],
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
import Sessions from '@abloatai/ablo/sessions';
|
|
19
|
+
|
|
20
|
+
const sessions = Sessions({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
21
|
+
|
|
22
|
+
const session = () =>
|
|
23
|
+
sessions.create({
|
|
24
|
+
agent: { id: stableWorkerId },
|
|
25
|
+
groups: [workspaceGroup],
|
|
26
|
+
can: workerAccess,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const agent = Ablo({ schema, session });
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Session clients default to one reconnecting WebSocket for commits, claims,
|
|
33
|
+
observation, presence, and collaboration. API-key clients remain HTTP by
|
|
34
|
+
default, and bounded session work can select `transport: 'http'` explicitly.
|
|
35
|
+
Model calls do not open additional sockets.
|
|
36
|
+
|
|
37
|
+
An async session provider represents one renewable logical identity. The client
|
|
38
|
+
caches each short-lived credential until it approaches `expiresAt`, pre-mints a
|
|
39
|
+
replacement, and reconnects with that replacement when necessary. Durable
|
|
40
|
+
observation resumes from its acknowledged cursor across socket replacement.
|
|
41
|
+
A provider resolving `null` means the application login ended and terminates the
|
|
42
|
+
session; a thrown error remains transient. A static session object cannot renew
|
|
43
|
+
itself and ends when its bearer expires. In-flight commits whose outcome became
|
|
44
|
+
ambiguous still reject and can be retried with their original idempotency key.
|
|
45
|
+
|
|
46
|
+
The browser client now names its session route as
|
|
47
|
+
`session: { endpoint: '/api/ablo-session' }`; `authEndpoint` is removed. Public
|
|
48
|
+
connection scope is `groups`; public `syncGroups` is removed. The overlapping
|
|
49
|
+
`agents.create`, `join`, and `useJoin` lifecycles are also removed: connection
|
|
50
|
+
groups define visibility, `usePeers` reads presence, and row claims own
|
|
51
|
+
exclusion.
|
|
52
|
+
|
|
53
|
+
Internally, session contract, creation, handler, source normalization, and
|
|
54
|
+
credential renewal now live beneath one `sessions` boundary. HTTP bootstrap and
|
|
55
|
+
the live socket consume the same normalized session access, so credential
|
|
56
|
+
identity and renewal policy cannot diverge.
|
|
57
|
+
|
|
58
|
+
Session issuance no longer occupies a property on `Ablo(...)`. That client owns
|
|
59
|
+
the schema model namespace, so an application model named `sessions` works as
|
|
60
|
+
`ablo.sessions` like any other model. Issuance and lifecycle administration stay
|
|
61
|
+
server-only behind the explicit `@abloatai/ablo/sessions` import.
|
|
62
|
+
|
|
63
|
+
## 0.59.2
|
|
64
|
+
|
|
65
|
+
### Patch Changes
|
|
66
|
+
|
|
67
|
+
- Updated dependencies [0b2fff7]
|
|
68
|
+
- @abloatai/humans@0.59.2
|
|
69
|
+
- @abloatai/transaction@0.59.2
|
|
70
|
+
|
|
3
71
|
## 0.59.1
|
|
4
72
|
|
|
5
73
|
### Plans recognize completed database migrations
|
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ flattening the implementation into `packages/ablo`:
|
|
|
127
127
|
- `packages/ablo` is the branded public facade. Its files mostly re-export the
|
|
128
128
|
package that owns each API.
|
|
129
129
|
- `packages/transaction` owns the shared model-operation contracts and the
|
|
130
|
-
|
|
130
|
+
headless HTTP/WebSocket transport implementation.
|
|
131
131
|
- `packages/humans` owns the reactive WebSocket/local/React implementation.
|
|
132
132
|
|
|
133
133
|
That means searching only inside `packages/ablo/src` will not find the
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { Sessions, Sessions as default } from '@abloatai/transaction/sessions';
|
|
2
|
+
export type { AbloSession, CreateAgentSessionParams, CreateSessionParams, CreateUserSessionParams, RevokeSessionParams, RotateSessionParams, SessionCredential, SessionEndpoint, SessionHandler, SessionHandlerOptions, SessionProvider, SessionProviderResult, SessionRevocation, SessionRotation, SessionScope, SessionSource, SessionsClient, SessionsOptions, } from '@abloatai/transaction/sessions';
|
|
3
|
+
//# sourceMappingURL=sessions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,IAAI,OAAO,EAAE,MAAM,gCAAgC,CAAC;AAC/E,YAAY,EACV,WAAW,EACX,wBAAwB,EACxB,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,qBAAqB,EACrB,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,GAChB,MAAM,gCAAgC,CAAC"}
|
package/dist/sessions.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,IAAI,OAAO,EAAE,MAAM,gCAAgC,CAAC"}
|
package/docs/agent-messaging.md
CHANGED
|
@@ -75,11 +75,12 @@ await ablo.ready();
|
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
The secret `apiKey` is server-only. Browser clients must not receive it; live UIs
|
|
78
|
-
use the
|
|
78
|
+
use the reactive client with `session.endpoint`, which owns their WebSocket and
|
|
79
|
+
short-lived user-session renewal.
|
|
79
80
|
|
|
80
81
|
If your backend mints restricted agent tokens, register the database once from a
|
|
81
82
|
secret-key server process as above. Workers using the restricted token can then
|
|
82
|
-
construct `Ablo({ schema,
|
|
83
|
+
construct `Ablo({ schema, session, transport: "http" })` because the project
|
|
83
84
|
already has a registered data plane.
|
|
84
85
|
|
|
85
86
|
## Link a message to a claim
|
package/docs/agents.md
CHANGED
|
@@ -21,16 +21,61 @@ console.log(matching[0].title);
|
|
|
21
21
|
These are observational reads. Use `read({ id })` only when a later Ablo write
|
|
22
22
|
depends on that exact version and will pass it through `reads`.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
Most agents wake on a trigger, read what they need, write a result, and go idle.
|
|
25
|
+
Trusted service work uses an API key and plain HTTP. An agent that needs its own
|
|
26
|
+
scoped identity receives a session; session clients use one reconnecting
|
|
27
|
+
WebSocket by default. The server resolves the org, scope, and actor from the
|
|
28
|
+
credential on both carriers.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
Create scoped credentials with the server-only issuer:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import Sessions from '@abloatai/ablo/sessions';
|
|
34
|
+
|
|
35
|
+
const sessions = Sessions({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
API-key services use HTTP. Session agents use a multiplexed WebSocket without
|
|
39
|
+
installing the human materializer. People add the `humans()` plugin for a local reactive graph. All three operate on the same typed,
|
|
40
|
+
coordinated state and enter the same server-side commit and claim paths.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const session = () => sessions.create({
|
|
44
|
+
agent: { id: stableWorkerId },
|
|
45
|
+
can: workerAccess,
|
|
46
|
+
groups: [workspaceGroup],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const ablo = Ablo({
|
|
50
|
+
schema,
|
|
51
|
+
session,
|
|
52
|
+
cursorStore,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await ablo.ready();
|
|
56
|
+
|
|
57
|
+
for await (const delta of ablo.observe()) {
|
|
58
|
+
await applyToAgentState(delta);
|
|
59
|
+
await delta.checkpoint(); // persist the cursor, then acknowledge it
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The selected WebSocket is shared by commits, row claims and releases,
|
|
64
|
+
subscription changes, pushed deltas, presence, and collaboration events.
|
|
65
|
+
Reconnect sends the last checkpointed position, so an uncheckpointed delta is
|
|
66
|
+
eligible for redelivery. An unsupported protocol version closes explicitly
|
|
67
|
+
instead of silently falling back to a different wire dialect.
|
|
68
|
+
|
|
69
|
+
The client retains no delta backlog while `observe()` is inactive. Starting an
|
|
70
|
+
observer requests replay from the durable checkpoint. An active observer has a
|
|
71
|
+
bounded in-memory backlog; if it falls behind that bound, observation fails
|
|
72
|
+
explicitly and can be restarted from the same durable checkpoint.
|
|
73
|
+
|
|
74
|
+
The public operation names do not change with the carrier. For example,
|
|
75
|
+
`ablo.records.update(...)` and `ablo.records.claim(...)` are the same calls on
|
|
76
|
+
HTTP and WebSocket. HTTP remains available for point reads and administrative
|
|
77
|
+
resources; `context().onChange` uses POST/SSE when HTTP is selected and reuses
|
|
78
|
+
the socket when WebSocket is selected.
|
|
34
79
|
|
|
35
80
|
<Note>
|
|
36
81
|
Agents transact against your **pushed schema**, same as everyone — `ablo.records`
|
|
@@ -40,14 +85,14 @@ authenticates; the [schema](/installation) defines what you can call.
|
|
|
40
85
|
|
|
41
86
|
## The agent client
|
|
42
87
|
|
|
43
|
-
Same `Ablo()` entry point as everywhere else
|
|
44
|
-
socket
|
|
88
|
+
Same `Ablo()` entry point as everywhere else. An API-key client is HTTP: no
|
|
89
|
+
socket or connection state, just your schema and service credential.
|
|
45
90
|
|
|
46
91
|
```ts
|
|
47
92
|
import Ablo from "@abloatai/ablo";
|
|
48
93
|
import { schema } from "./schema";
|
|
49
94
|
|
|
50
|
-
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY
|
|
95
|
+
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
51
96
|
|
|
52
97
|
// Reads + writes, fully typed off your schema.
|
|
53
98
|
// `get` resolves to the row, or `undefined` when none matches.
|
|
@@ -62,17 +107,16 @@ await ablo.records.update({ id: record.id, data: { status: "done" } });
|
|
|
62
107
|
|
|
63
108
|
It exposes `get` / `list` / `create` / `update` / `delete`, plus `commits`
|
|
64
109
|
and `claim`. It does **not** expose stateful-only `local` reads or model
|
|
65
|
-
`onChange` subscriptions.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
listener stops.
|
|
110
|
+
`onChange` subscriptions. `context().onChange` is separate: it reuses the
|
|
111
|
+
selected WebSocket transport, or holds one POST/SSE response until the context
|
|
112
|
+
changes on the HTTP transport.
|
|
69
113
|
|
|
70
|
-
##
|
|
114
|
+
## Scoped agent sessions
|
|
71
115
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
116
|
+
`Sessions(...).create({ agent })` is the one issuance path. It returns a restricted
|
|
117
|
+
credential; construct the schema-typed client in the runtime that executes the
|
|
118
|
+
agent. This keeps session minting, revocation, and rotation under one resource
|
|
119
|
+
for both people and agents.
|
|
76
120
|
|
|
77
121
|
Derive identity and groups from the run row or trusted job payload—not from
|
|
78
122
|
model output or an HTTP request body. A serverless handler normally creates and
|
|
@@ -82,12 +126,14 @@ disposes one child per invocation:
|
|
|
82
126
|
const run = await control.runs.read({ id: verifiedRunId });
|
|
83
127
|
if (!run) throw new Error('run not found');
|
|
84
128
|
|
|
85
|
-
const
|
|
86
|
-
id: `run:${run.id}
|
|
87
|
-
name: 'run-worker',
|
|
129
|
+
const session = await sessions.create({
|
|
130
|
+
agent: { id: `run:${run.id}` },
|
|
88
131
|
can: { records: ['read', 'update'] },
|
|
89
|
-
|
|
132
|
+
groups: [`workspace:${run.workspaceId}`],
|
|
133
|
+
userMeta: { name: 'run-worker' },
|
|
90
134
|
});
|
|
135
|
+
|
|
136
|
+
const agent = Ablo({ schema, session, transport: 'http' });
|
|
91
137
|
try {
|
|
92
138
|
await executeRun(agent, run);
|
|
93
139
|
} finally {
|
|
@@ -96,29 +142,32 @@ try {
|
|
|
96
142
|
```
|
|
97
143
|
|
|
98
144
|
Use a stable id only when one logical run is serialized; two concurrent workers
|
|
99
|
-
that share an id appear as the same participant.
|
|
100
|
-
|
|
145
|
+
that share an id appear as the same participant. Generate a different id for
|
|
146
|
+
each independent concurrent run.
|
|
101
147
|
|
|
102
|
-
A long-running worker may cache one
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
reuse
|
|
148
|
+
A long-running worker may cache one client per stable scope and provide an
|
|
149
|
+
async credential resolver that re-mints the same session identity. The cache
|
|
150
|
+
owns lifecycle: evict idle clients, call `dispose()` on eviction, and dispose
|
|
151
|
+
every client during graceful shutdown. Never reuse a client for a different
|
|
152
|
+
workspace or capability set.
|
|
106
153
|
|
|
107
154
|
```ts
|
|
108
155
|
const agents: Record<
|
|
109
156
|
string,
|
|
110
|
-
|
|
157
|
+
ReturnType<typeof Ablo> | undefined
|
|
111
158
|
> = {};
|
|
112
159
|
|
|
113
160
|
async function agentFor(run: Run) {
|
|
114
161
|
const key = `${run.workspaceId}:${run.workerSlot}`;
|
|
115
162
|
const cached = agents[key];
|
|
116
163
|
if (cached) return cached;
|
|
117
|
-
const
|
|
118
|
-
|
|
164
|
+
const agentId = `worker:${key}`;
|
|
165
|
+
const session = () => sessions.create({
|
|
166
|
+
agent: { id: agentId },
|
|
119
167
|
can: { records: ['read', 'update'] },
|
|
120
|
-
|
|
168
|
+
groups: [`workspace:${run.workspaceId}`],
|
|
121
169
|
});
|
|
170
|
+
const created = Ablo({ schema, session });
|
|
122
171
|
agents[key] = created;
|
|
123
172
|
return created;
|
|
124
173
|
}
|
package/docs/api-keys.md
CHANGED
|
@@ -28,7 +28,7 @@ and remap them before each command.
|
|
|
28
28
|
| Prepare a branch once, including CI | expiring `sk_` bound to that branch | `npx ablo dev --no-watch --branch <ref>`; headless CI supplies an `mk_` credential through `ABLO_API_KEY`. |
|
|
29
29
|
| Run the production backend | `sk_` bound to the production root | Store it as the deployment's `ABLO_API_KEY`. |
|
|
30
30
|
| Read in a browser | `pk_` | Publishable, read-only key. |
|
|
31
|
-
| Write in a browser as a user | short-lived `ek_` | Your backend exposes
|
|
31
|
+
| Write in a browser as a user | short-lived `ek_` | Your backend exposes a session endpoint and mints it. |
|
|
32
32
|
|
|
33
33
|
The everyday loop is therefore:
|
|
34
34
|
|
|
@@ -71,16 +71,18 @@ already carries the target.
|
|
|
71
71
|
|
|
72
72
|
## Which credential to pass to the SDK
|
|
73
73
|
|
|
74
|
-
There
|
|
75
|
-
Pick your row:
|
|
74
|
+
There are two ordinary identity inputs: `apiKey` for a key the process owns and
|
|
75
|
+
`session` for a scoped actor. Pick your row:
|
|
76
76
|
|
|
77
77
|
| Where your code runs | What to pass | Example |
|
|
78
78
|
|---|---|---|
|
|
79
79
|
| **Server / worker / agent** (can hold a secret) | your secret `sk_`: it defaults to `ABLO_API_KEY`, so usually pass **nothing** | `Ablo({ schema })` |
|
|
80
80
|
| **Browser: read-only** | a publishable `pk_` (safe to ship) | `Ablo({ schema, apiKey: process.env.NEXT_PUBLIC_ABLO_PUBLISHABLE_KEY })` |
|
|
81
|
-
| **Browser: writing as the signed-in user** | `
|
|
81
|
+
| **Browser: writing as the signed-in user** | `session.endpoint`: the route on your own backend that mints a short-lived per-user token | `Ablo({ schema, session: { endpoint: '/api/ablo-session' } })` |
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
The names follow ownership: a process owns an API key; an actor runs through a
|
|
84
|
+
session, regardless of whether that session is already minted, renewable, or
|
|
85
|
+
fetched from a browser endpoint.
|
|
84
86
|
|
|
85
87
|
The `mk_` credential created by `ablo login` is different: it is a CLI
|
|
86
88
|
control-plane credential, not an application API key. It can manage projects
|
|
@@ -104,24 +106,28 @@ For an `ek_`, the server mints and the client holds the short-lived result.
|
|
|
104
106
|
public `pk_` is **read-only** — it can't carry one specific user's write authority. So when
|
|
105
107
|
the browser writes *as the logged-in user*, your backend (which holds the secret `sk_` and
|
|
106
108
|
knows who's signed in) mints a short-lived per-user token with `sessions.create({ user, can })`,
|
|
107
|
-
and the browser's `
|
|
109
|
+
and the browser's `session.endpoint` fetches it. You don't manage refresh — the SDK calls the
|
|
108
110
|
function once before connecting and then keeps the token fresh (re-mint before expiry, and on
|
|
109
111
|
tab-focus / network-online / device-wake). For a read-only app you don't need
|
|
110
112
|
any of this — just the `pk_` above.
|
|
111
113
|
|
|
112
114
|
Server-side, because `apiKey` defaults to `process.env.ABLO_API_KEY`, most backend and agent
|
|
113
115
|
code passes nothing. The secret `sk_` is **server-only** — never in a
|
|
114
|
-
browser bundle. There is no `getToken
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
browser bundle. There is no `getToken`, `as`, or separate auth-endpoint option:
|
|
117
|
+
`apiKey` is the key a process owns, while `session` is a minted resource, a
|
|
118
|
+
renewal provider, or `{ endpoint }`. Set exactly one identity input.
|
|
117
119
|
|
|
118
120
|
### Minting per-user / agent tokens (server-side, with your `sk_`)
|
|
119
121
|
|
|
122
|
+
Construct the dedicated issuer with
|
|
123
|
+
`Sessions({ schema, apiKey: process.env.ABLO_API_KEY })` from
|
|
124
|
+
`@abloatai/ablo/sessions`. It is server-only and does not create a participant
|
|
125
|
+
connection.
|
|
126
|
+
|
|
120
127
|
| Mint | Call | Result |
|
|
121
128
|
|---|---|---|
|
|
122
|
-
| Human end-user session | `await
|
|
123
|
-
|
|
|
124
|
-
| Raw delegated agent token | `await server.sessions.create({ agent: { id }, can: { records: ['update'] } })` | `rk_` for another runtime |
|
|
129
|
+
| Human end-user session | `await sessions.create({ user: { id }, can: { records: ['read'] } })` | `ek_` (scoped to `can`) |
|
|
130
|
+
| Agent session | `await sessions.create({ agent: { id }, can: { records: ['update'] } })` | Scoped `rk_` for the agent runtime |
|
|
125
131
|
|
|
126
132
|
The principal kind comes from *which* shape you pass — `{ user, can }` → `user`, `{ agent, can }` → `agent`.
|
|
127
133
|
|
package/docs/client-behavior.md
CHANGED
|
@@ -28,10 +28,12 @@ const ablo = Ablo({
|
|
|
28
28
|
});
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
The package-root export is the
|
|
32
|
-
handlers, and other server operations.
|
|
33
|
-
|
|
34
|
-
[
|
|
31
|
+
The package-root export is the headless coordination client for agents, workers,
|
|
32
|
+
route handlers, and other server operations. Trusted API-key clients use HTTP.
|
|
33
|
+
Scoped session clients use one reconnecting WebSocket for commits and live
|
|
34
|
+
coordination; point reads and administration remain HTTP. See [Transports](./transports.md)
|
|
35
|
+
for the lifecycle and [Options](./options.md) for the constructor. A human-facing
|
|
36
|
+
local graph is added through the [React client](./react.md).
|
|
35
37
|
|
|
36
38
|
Your database connects out of band — through logical replication (`npx ablo
|
|
37
39
|
connect`), or the signed [Data Source](./data-sources.md) endpoint as the
|
package/docs/coordination.md
CHANGED
|
@@ -206,11 +206,15 @@ same credential represent the same participant and do not exclude one another.
|
|
|
206
206
|
Mint a distinct scoped session for each independently coordinated agent:
|
|
207
207
|
|
|
208
208
|
```ts
|
|
209
|
-
|
|
209
|
+
import Sessions from '@abloatai/ablo/sessions';
|
|
210
|
+
|
|
211
|
+
const sessions = Sessions({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
212
|
+
const session = await sessions.create({
|
|
210
213
|
agent: { id: `forecast-agent-${workerId}` },
|
|
214
|
+
can: { records: ['read', 'update'] },
|
|
211
215
|
});
|
|
212
216
|
|
|
213
|
-
const agent = Ablo({ schema,
|
|
217
|
+
const agent = Ablo({ schema, session });
|
|
214
218
|
```
|
|
215
219
|
|
|
216
220
|
Functional updates do not require distinct participant identities because they
|
|
@@ -305,7 +309,6 @@ The main methods are:
|
|
|
305
309
|
| `claim.state({ id })` | Read the current holder without blocking. |
|
|
306
310
|
| `claim.queue({ id })` | Read the current wait order. |
|
|
307
311
|
| `claim.release({ id })` | Release early when you do not hold a handle. |
|
|
308
|
-
| `join({ scope })` | Observe presence for a broader scope. |
|
|
309
312
|
|
|
310
313
|
This page owns row-backed claims: `model.claim({ id })` reads and claims an Ablo
|
|
311
314
|
model row, and the handle carries fresh data. Identifier-only claims before an
|
|
@@ -47,27 +47,16 @@ export const schema = defineSchema(
|
|
|
47
47
|
```ts
|
|
48
48
|
// 2. app/api/ablo-session/route.ts — mint for one customer, on your backend.
|
|
49
49
|
import { syncGroup } from '@abloatai/ablo/schema';
|
|
50
|
-
import {
|
|
51
|
-
import { ablo } from '@/ablo/server';
|
|
50
|
+
import { sessions } from '@/ablo/sessions';
|
|
52
51
|
|
|
53
|
-
export
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const session = await ablo.sessions.create({
|
|
52
|
+
export const POST = sessions.handler({
|
|
53
|
+
authenticate: () => currentSignedInMember(),
|
|
54
|
+
grant: ({ principal: member }) => ({
|
|
57
55
|
user: { id: member.userId },
|
|
58
56
|
can: { customers: ['read'], decks: ['read', 'create', 'update'] },
|
|
59
|
-
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
return Response.json(
|
|
63
|
-
credentialEndpointSuccessSchema.parse({
|
|
64
|
-
token: session.token,
|
|
65
|
-
expiresAt: session.expiresAt,
|
|
66
|
-
credentialKind: 'ephemeral',
|
|
67
|
-
}),
|
|
68
|
-
{ headers: { 'Cache-Control': 'no-store' } },
|
|
69
|
-
);
|
|
70
|
-
}
|
|
57
|
+
groups: [syncGroup('customer', member.customerId)],
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
71
60
|
```
|
|
72
61
|
|
|
73
62
|
That is the whole integration. The rest of this page is why each line is where
|
|
@@ -130,7 +119,7 @@ kind is the one you declared in `groups.root`, and the id is your own
|
|
|
130
119
|
identifier for the customer.
|
|
131
120
|
|
|
132
121
|
```ts
|
|
133
|
-
|
|
122
|
+
groups: [syncGroup('customer', member.customerId)]
|
|
134
123
|
```
|
|
135
124
|
|
|
136
125
|
Resolve `member.customerId` from the membership you just authenticated on the
|
package/docs/deployment.md
CHANGED
|
@@ -120,15 +120,17 @@ is genuinely unavailable.
|
|
|
120
120
|
|
|
121
121
|
## 2. The credential each runtime holds
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
Credential configuration follows the runtime. Long-lived root keys use
|
|
124
|
+
`apiKey`; scoped actors use `session`; browser login exchange uses
|
|
125
|
+
`session.endpoint`:
|
|
125
126
|
|
|
126
127
|
| Runtime | Credential | Notes |
|
|
127
128
|
|---|---|---|
|
|
128
129
|
| Server, worker, agent, cron | `sk_` in `ABLO_API_KEY` | Defaults from the environment, so most code passes nothing. |
|
|
129
|
-
| Serverless function | `sk_` in `ABLO_API_KEY
|
|
130
|
+
| Serverless function | `sk_` in `ABLO_API_KEY` | Stateless request/response; nothing held open across invocations. |
|
|
131
|
+
| Long-running agent | `session: () => sessions.create(...)` | One renewable identity and one multiplexed WebSocket per client and Ablo cell; checkpoint durable deltas before acknowledging. |
|
|
130
132
|
| Browser, read-only | root-bound `pk_` | Publishable, safe to ship, and read-only. |
|
|
131
|
-
| Browser, writing as the signed-in user | `
|
|
133
|
+
| Browser, writing as the signed-in user | `session: { endpoint }` | A route on your backend mints a short-lived `ek_` per user. |
|
|
132
134
|
|
|
133
135
|
[API Keys](./api-keys.md) covers the model; [Sessions](./sessions.md) covers
|
|
134
136
|
minting. Two things bite specifically at deploy time.
|
|
@@ -283,7 +285,7 @@ and what each promises.
|
|
|
283
285
|
browser bundle.
|
|
284
286
|
3. `ablo plan` reviewed, followed by fingerprint-gated `ablo push --yes`.
|
|
285
287
|
4. `ablo status --json` gating the deploy on an empty `blockers` array.
|
|
286
|
-
5. Browser clients on a root-bound `pk_` or
|
|
288
|
+
5. Browser clients on a root-bound `pk_` or `session.endpoint`, not a secret key.
|
|
287
289
|
6. Webhook endpoints registered at their deployed URLs, with the signing secret
|
|
288
290
|
in your environment.
|
|
289
291
|
|
|
@@ -48,12 +48,18 @@ export const schema = defineSchema({
|
|
|
48
48
|
```ts
|
|
49
49
|
// web/ablo.ts — SERVER-ONLY client (holds the sk_ key; never imported in the browser).
|
|
50
50
|
import Ablo from '@abloatai/ablo';
|
|
51
|
+
import Sessions from '@abloatai/ablo/sessions';
|
|
51
52
|
import { schema } from './ablo/schema';
|
|
52
53
|
|
|
53
54
|
export const ablo = Ablo({
|
|
54
55
|
schema,
|
|
55
56
|
apiKey: process.env.ABLO_API_KEY,
|
|
56
57
|
});
|
|
58
|
+
|
|
59
|
+
export const sessions = Sessions({
|
|
60
|
+
schema,
|
|
61
|
+
apiKey: process.env.ABLO_API_KEY,
|
|
62
|
+
});
|
|
57
63
|
```
|
|
58
64
|
|
|
59
65
|
Mount the React provider near the app root. Build the browser client first —
|
|
@@ -69,11 +75,11 @@ import { Ablo } from '@abloatai/ablo/react';
|
|
|
69
75
|
import { AbloProvider } from '@abloatai/ablo/react';
|
|
70
76
|
import { schema } from '@/ablo/schema';
|
|
71
77
|
|
|
72
|
-
// Browser client: no secret key — `
|
|
78
|
+
// Browser client: no secret key — `session.endpoint` points at the session route
|
|
73
79
|
// your server exposes (below); the SDK fetches and refreshes the token.
|
|
74
80
|
const ablo = Ablo({
|
|
75
81
|
schema,
|
|
76
|
-
|
|
82
|
+
session: { endpoint: '/api/ablo-session' },
|
|
77
83
|
});
|
|
78
84
|
|
|
79
85
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
@@ -86,26 +92,17 @@ browser only ever sees the short-lived token:
|
|
|
86
92
|
|
|
87
93
|
```ts
|
|
88
94
|
// web/app/api/ablo-session/route.ts
|
|
89
|
-
import {
|
|
90
|
-
import { credentialEndpointSuccessSchema } from '@abloatai/ablo/auth';
|
|
95
|
+
import { sessions } from '@/ablo';
|
|
91
96
|
|
|
92
97
|
export const runtime = 'nodejs';
|
|
93
98
|
|
|
94
|
-
export
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
user: { id:
|
|
99
|
+
export const POST = sessions.handler({
|
|
100
|
+
authenticate: () => currentUser(), // your auth; null when signed out
|
|
101
|
+
grant: ({ principal: user }) => ({
|
|
102
|
+
user: { id: user.id },
|
|
98
103
|
can: { records: ['read', 'update'] },
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
credentialEndpointSuccessSchema.parse({
|
|
102
|
-
token,
|
|
103
|
-
expiresAt,
|
|
104
|
-
credentialKind: 'ephemeral',
|
|
105
|
-
}),
|
|
106
|
-
{ headers: { 'Cache-Control': 'no-store' } },
|
|
107
|
-
);
|
|
108
|
-
}
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
109
106
|
```
|
|
110
107
|
|
|
111
108
|
## 2. Add Live Reads In The UI
|