@abloatai/ablo 0.59.2 → 0.61.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 +77 -0
- package/README.md +1 -1
- package/dist/ai-sdk.js.map +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 +49 -32
- package/docs/api-keys.md +18 -12
- package/docs/cli.md +1 -0
- package/docs/client-behavior.md +6 -4
- package/docs/coordination.md +6 -3
- package/docs/customer-organizations.md +8 -19
- package/docs/data-sources.md +42 -15
- package/docs/deployment.md +7 -6
- 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 +80 -29
- 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,82 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.61.0
|
|
4
|
+
|
|
5
|
+
### Existing database connections repair in place
|
|
6
|
+
|
|
7
|
+
`ablo connect apply` is now the single repeatable operation for both a new
|
|
8
|
+
database connection and an existing registration. A healthy connection is a
|
|
9
|
+
no-op. When a registration predates current publication, replica-identity,
|
|
10
|
+
grant, or row-level-security requirements, the same command reconciles those
|
|
11
|
+
database-owned invariants through the transient owner URL while preserving
|
|
12
|
+
Ablo's working scoped passwords.
|
|
13
|
+
|
|
14
|
+
If that repair means an earlier initial snapshot may have omitted rows, the
|
|
15
|
+
operation requests the required fresh snapshot and subsequent reruns report its
|
|
16
|
+
loading or ready state. Automation can select `--json` for stable lifecycle and
|
|
17
|
+
per-step codes instead of parsing human output. Credential rotation remains an
|
|
18
|
+
explicit operation for an actually incomplete or invalid role pair.
|
|
19
|
+
|
|
20
|
+
## 0.60.0
|
|
21
|
+
|
|
22
|
+
### Sessions are the connection boundary for people and agents
|
|
23
|
+
|
|
24
|
+
`Sessions({ schema, apiKey })` is now the dedicated session issuer. Backends create scoped
|
|
25
|
+
agent sessions with `sessions.create({ agent, can, groups })` and expose browser
|
|
26
|
+
sessions with `sessions.handler({ authenticate, grant })`. Both return the same
|
|
27
|
+
short-lived session contract, and both are supplied to clients through
|
|
28
|
+
`Ablo({ schema, session })`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const workerAccess = {
|
|
32
|
+
records: ['read', 'update'],
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
import Sessions from '@abloatai/ablo/sessions';
|
|
36
|
+
|
|
37
|
+
const sessions = Sessions({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
38
|
+
|
|
39
|
+
const session = () =>
|
|
40
|
+
sessions.create({
|
|
41
|
+
agent: { id: stableWorkerId },
|
|
42
|
+
groups: [workspaceGroup],
|
|
43
|
+
can: workerAccess,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const agent = Ablo({ schema, session });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Session clients default to one reconnecting WebSocket for commits, claims,
|
|
50
|
+
observation, presence, and collaboration. API-key clients remain HTTP by
|
|
51
|
+
default, and bounded session work can select `transport: 'http'` explicitly.
|
|
52
|
+
Model calls do not open additional sockets.
|
|
53
|
+
|
|
54
|
+
An async session provider represents one renewable logical identity. The client
|
|
55
|
+
caches each short-lived credential until it approaches `expiresAt`, pre-mints a
|
|
56
|
+
replacement, and reconnects with that replacement when necessary. Durable
|
|
57
|
+
observation resumes from its acknowledged cursor across socket replacement.
|
|
58
|
+
A provider resolving `null` means the application login ended and terminates the
|
|
59
|
+
session; a thrown error remains transient. A static session object cannot renew
|
|
60
|
+
itself and ends when its bearer expires. In-flight commits whose outcome became
|
|
61
|
+
ambiguous still reject and can be retried with their original idempotency key.
|
|
62
|
+
|
|
63
|
+
The browser client now names its session route as
|
|
64
|
+
`session: { endpoint: '/api/ablo-session' }`; `authEndpoint` is removed. Public
|
|
65
|
+
connection scope is `groups`; public `syncGroups` is removed. The overlapping
|
|
66
|
+
`agents.create`, `join`, and `useJoin` lifecycles are also removed: connection
|
|
67
|
+
groups define visibility, `usePeers` reads presence, and row claims own
|
|
68
|
+
exclusion.
|
|
69
|
+
|
|
70
|
+
Internally, session contract, creation, handler, source normalization, and
|
|
71
|
+
credential renewal now live beneath one `sessions` boundary. HTTP bootstrap and
|
|
72
|
+
the live socket consume the same normalized session access, so credential
|
|
73
|
+
identity and renewal policy cannot diverge.
|
|
74
|
+
|
|
75
|
+
Session issuance no longer occupies a property on `Ablo(...)`. That client owns
|
|
76
|
+
the schema model namespace, so an application model named `sessions` works as
|
|
77
|
+
`ablo.sessions` like any other model. Issuance and lifecycle administration stay
|
|
78
|
+
server-only behind the explicit `@abloatai/ablo/sessions` import.
|
|
79
|
+
|
|
3
80
|
## 0.59.2
|
|
4
81
|
|
|
5
82
|
### Patch Changes
|
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
|
package/dist/ai-sdk.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-sdk.js","sourceRoot":"","sources":["../src/ai-sdk.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAE7C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACnD,CAAC,CAAC;AAYH,kFAAkF;AAClF,MAAM,UAAU,cAAc,CAC5B,KAA2B,EAC3B,
|
|
1
|
+
{"version":3,"file":"ai-sdk.js","sourceRoot":"","sources":["../src/ai-sdk.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAE7C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACnD,CAAC,CAAC;AAYH,kFAAkF;AAClF,MAAM,UAAU,cAAc,CAC5B,KAA2B,EAC3B,UAAwC,EAAE;IAE1C,MAAM,EAAE,OAAO,EAAE,GAAG,2BAA2B,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/D,MAAM,IAAI,GAAG,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CACjC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CACzE,CAAC;IACF,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAC5B,QAAQ,EACR,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,EACjE,CAAC,CACF,CAAC;IACF,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,0DAA0D,OAAO,EAAE;KAC7E,CAAC;AACJ,CAAC"}
|
|
@@ -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
|
@@ -22,21 +22,33 @@ 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
24
|
Most agents wake on a trigger, read what they need, write a result, and go idle.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
|
|
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,
|
|
33
40
|
coordinated state and enter the same server-side commit and claim paths.
|
|
34
41
|
|
|
35
42
|
```ts
|
|
43
|
+
const session = () => sessions.create({
|
|
44
|
+
agent: { id: stableWorkerId },
|
|
45
|
+
can: workerAccess,
|
|
46
|
+
groups: [workspaceGroup],
|
|
47
|
+
});
|
|
48
|
+
|
|
36
49
|
const ablo = Ablo({
|
|
37
50
|
schema,
|
|
38
|
-
|
|
39
|
-
transport: 'websocket',
|
|
51
|
+
session,
|
|
40
52
|
cursorStore,
|
|
41
53
|
});
|
|
42
54
|
|
|
@@ -73,14 +85,14 @@ authenticates; the [schema](/installation) defines what you can call.
|
|
|
73
85
|
|
|
74
86
|
## The agent client
|
|
75
87
|
|
|
76
|
-
Same `Ablo()` entry point as everywhere else
|
|
77
|
-
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.
|
|
78
90
|
|
|
79
91
|
```ts
|
|
80
92
|
import Ablo from "@abloatai/ablo";
|
|
81
93
|
import { schema } from "./schema";
|
|
82
94
|
|
|
83
|
-
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY
|
|
95
|
+
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
84
96
|
|
|
85
97
|
// Reads + writes, fully typed off your schema.
|
|
86
98
|
// `get` resolves to the row, or `undefined` when none matches.
|
|
@@ -99,12 +111,12 @@ and `claim`. It does **not** expose stateful-only `local` reads or model
|
|
|
99
111
|
selected WebSocket transport, or holds one POST/SSE response until the context
|
|
100
112
|
changes on the HTTP transport.
|
|
101
113
|
|
|
102
|
-
##
|
|
114
|
+
## Scoped agent sessions
|
|
103
115
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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.
|
|
108
120
|
|
|
109
121
|
Derive identity and groups from the run row or trusted job payload—not from
|
|
110
122
|
model output or an HTTP request body. A serverless handler normally creates and
|
|
@@ -114,12 +126,14 @@ disposes one child per invocation:
|
|
|
114
126
|
const run = await control.runs.read({ id: verifiedRunId });
|
|
115
127
|
if (!run) throw new Error('run not found');
|
|
116
128
|
|
|
117
|
-
const
|
|
118
|
-
id: `run:${run.id}
|
|
119
|
-
name: 'run-worker',
|
|
129
|
+
const session = await sessions.create({
|
|
130
|
+
agent: { id: `run:${run.id}` },
|
|
120
131
|
can: { records: ['read', 'update'] },
|
|
121
|
-
|
|
132
|
+
groups: [`workspace:${run.workspaceId}`],
|
|
133
|
+
userMeta: { name: 'run-worker' },
|
|
122
134
|
});
|
|
135
|
+
|
|
136
|
+
const agent = Ablo({ schema, session, transport: 'http' });
|
|
123
137
|
try {
|
|
124
138
|
await executeRun(agent, run);
|
|
125
139
|
} finally {
|
|
@@ -128,29 +142,32 @@ try {
|
|
|
128
142
|
```
|
|
129
143
|
|
|
130
144
|
Use a stable id only when one logical run is serialized; two concurrent workers
|
|
131
|
-
that share an id appear as the same participant.
|
|
132
|
-
|
|
145
|
+
that share an id appear as the same participant. Generate a different id for
|
|
146
|
+
each independent concurrent run.
|
|
133
147
|
|
|
134
|
-
A long-running worker may cache one
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
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.
|
|
138
153
|
|
|
139
154
|
```ts
|
|
140
155
|
const agents: Record<
|
|
141
156
|
string,
|
|
142
|
-
|
|
157
|
+
ReturnType<typeof Ablo> | undefined
|
|
143
158
|
> = {};
|
|
144
159
|
|
|
145
160
|
async function agentFor(run: Run) {
|
|
146
161
|
const key = `${run.workspaceId}:${run.workerSlot}`;
|
|
147
162
|
const cached = agents[key];
|
|
148
163
|
if (cached) return cached;
|
|
149
|
-
const
|
|
150
|
-
|
|
164
|
+
const agentId = `worker:${key}`;
|
|
165
|
+
const session = () => sessions.create({
|
|
166
|
+
agent: { id: agentId },
|
|
151
167
|
can: { records: ['read', 'update'] },
|
|
152
|
-
|
|
168
|
+
groups: [`workspace:${run.workspaceId}`],
|
|
153
169
|
});
|
|
170
|
+
const created = Ablo({ schema, session });
|
|
154
171
|
agents[key] = created;
|
|
155
172
|
return created;
|
|
156
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/cli.md
CHANGED
|
@@ -136,6 +136,7 @@ branch-bound runtime credential before starting application code.
|
|
|
136
136
|
| `ablo migrate` | **Direct Postgres**: provision just the synced models (plus the adapter's `ablo_outbox` / `ablo_idempotency`) in your own `DATABASE_URL`. Leaves your other tables alone. | `--dry-run`, `--output <file>`, `--schema`, `--export` |
|
|
137
137
|
| `ablo pull` | **Direct Postgres**: generate `defineSchema(...)` from your existing tables (read-only, like `prisma db pull`). | `--out <path>`, `--app-schema <name>`, `--import <pkg>`, `--force` |
|
|
138
138
|
| `ablo check` | **Direct Postgres**: verify your _existing_ tables fit the schema (read-only, no schema changes). | `--schema <path>`, `--export <name>`, `--app-schema <name>` |
|
|
139
|
+
| `ablo connect plan\|apply\|check\|rotate\|deregister` | **Direct Postgres**: register and maintain a database plane. Here `--schema` means the existing PostgreSQL namespace, never a TypeScript file. | `--url <postgres-url>`, `--schema <postgres-schema>`, `--env-file <path>`, `--yes` |
|
|
139
140
|
| `ablo generate` | Emit TypeScript types from the schema. | `--out <path>`, `--schema`, `--export` |
|
|
140
141
|
| `ablo docs` | Read these pages for the version you installed: offline, no network (see [`ablo docs`](#ablo-docs)). | `--json` |
|
|
141
142
|
|
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/data-sources.md
CHANGED
|
@@ -132,6 +132,27 @@ npx ablo connect apply --env-file .env.local --yes
|
|
|
132
132
|
The explicit flag makes the credential choice visible and loads both the
|
|
133
133
|
branch-bound key and database URL. Shell environment variables take precedence.
|
|
134
134
|
|
|
135
|
+
### Two schema flags, two ownership boundaries
|
|
136
|
+
|
|
137
|
+
`migrate` and `push` read your Ablo TypeScript contract. `connect apply` also
|
|
138
|
+
reads it to derive the mapped Postgres tables; `--tables` is an explicit
|
|
139
|
+
override:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# TypeScript contract and export; app-schema selects its Postgres namespace.
|
|
143
|
+
npx ablo migrate --schema ablo/schema.ts --export schema --app-schema public
|
|
144
|
+
|
|
145
|
+
# Existing Postgres namespace only. Never pass ablo/schema.ts here.
|
|
146
|
+
npx ablo connect apply --schema public --yes
|
|
147
|
+
|
|
148
|
+
# Activate the TypeScript contract after the database is connected.
|
|
149
|
+
npx ablo push --schema ablo/schema.ts --export schema
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
If the database uses its default namespace, omit `connect --schema`; it defaults
|
|
153
|
+
to `public`. Keep the three operations in this order in deployment automation:
|
|
154
|
+
expand tables, connect the database, then activate the hosted schema.
|
|
155
|
+
|
|
135
156
|
### One database, several projects
|
|
136
157
|
|
|
137
158
|
Provider database URLs and Postgres schemas solve different isolation jobs:
|
|
@@ -190,13 +211,18 @@ npx ablo connect apply --url postgres://admin:...@host:5432/db --schema mail
|
|
|
190
211
|
```
|
|
191
212
|
|
|
192
213
|
Pass an admin connection string with `--url` and select the application namespace
|
|
193
|
-
with `--schema` (default `public`). It
|
|
194
|
-
per-binding scoped roles,
|
|
195
|
-
|
|
214
|
+
with `--schema` (default `public`). It inspects and reconciles a per-binding
|
|
215
|
+
publication, two per-binding scoped roles, grants, replica identity,
|
|
216
|
+
registration, and the initial snapshot. On an existing healthy registration it
|
|
217
|
+
is a no-op and preserves both passwords. A policy-only repair also preserves
|
|
218
|
+
them; credential rotation happens only through the explicit `connect rotate`
|
|
219
|
+
operation.
|
|
220
|
+
|
|
196
221
|
The admin credential is used on this machine only and never persisted — nothing is
|
|
197
|
-
written to your `.env`, which keeps holding only `ABLO_API_KEY`. Pass
|
|
198
|
-
to see every statement first,
|
|
199
|
-
|
|
222
|
+
written to your `.env`, which keeps holding only `ABLO_API_KEY`. Pass
|
|
223
|
+
`--show-sql` to see every statement first, `--json` for stable result and step
|
|
224
|
+
codes, or drop `--apply` to print the SQL and run it yourself. Rotate the scoped
|
|
225
|
+
passwords any time with `ablo connect rotate`.
|
|
200
226
|
|
|
201
227
|
The rest of this page is what that command sets up, step by step, for when you want
|
|
202
228
|
to run it by hand or review exactly what changes.
|
|
@@ -215,19 +241,20 @@ the row to be visible already, and touching application rows is neither necessar
|
|
|
215
241
|
nor a safe bootstrap mechanism.
|
|
216
242
|
|
|
217
243
|
If a connection was snapshotted with an older replication role whose row-level
|
|
218
|
-
security hid historical rows,
|
|
219
|
-
deregistering or rotating
|
|
244
|
+
security hid historical rows, rerun the same operation. It repairs the owned
|
|
245
|
+
policy and requests the necessary fresh load without deregistering or rotating
|
|
246
|
+
credentials:
|
|
220
247
|
|
|
221
248
|
```bash
|
|
222
|
-
npx ablo connect
|
|
223
|
-
|
|
224
|
-
npx ablo connect check # repeat until the existing-row load is complete
|
|
249
|
+
npx ablo connect apply --url postgres://owner:...@host/db --yes --json
|
|
250
|
+
# Rerun the same command: loading becomes ready; a healthy rerun is a no-op.
|
|
225
251
|
```
|
|
226
252
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
253
|
+
Rerun `connect apply` after adding an existing populated table to the contract.
|
|
254
|
+
Following its future WAL changes is not enough to load rows written before
|
|
255
|
+
publication membership, so reconciliation updates membership and requests the
|
|
256
|
+
fresh snapshot together. The snapshot coverage guard refuses to record
|
|
257
|
+
completion when even one mapped table is absent. Relation matching is
|
|
231
258
|
schema-qualified using the DataSource's configured `schema` (default `public`):
|
|
232
259
|
an identically named table in another Postgres schema neither counts as coverage
|
|
233
260
|
nor enters the snapshot or WAL stream for your model.
|
package/docs/deployment.md
CHANGED
|
@@ -120,16 +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
|
-
|
|
|
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. |
|
|
131
132
|
| Browser, read-only | root-bound `pk_` | Publishable, safe to ship, and read-only. |
|
|
132
|
-
| 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. |
|
|
133
134
|
|
|
134
135
|
[API Keys](./api-keys.md) covers the model; [Sessions](./sessions.md) covers
|
|
135
136
|
minting. Two things bite specifically at deploy time.
|
|
@@ -284,7 +285,7 @@ and what each promises.
|
|
|
284
285
|
browser bundle.
|
|
285
286
|
3. `ablo plan` reviewed, followed by fingerprint-gated `ablo push --yes`.
|
|
286
287
|
4. `ablo status --json` gating the deploy on an empty `blockers` array.
|
|
287
|
-
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.
|
|
288
289
|
6. Webhook endpoints registered at their deployed URLs, with the signing secret
|
|
289
290
|
in your environment.
|
|
290
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
|