@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 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
- stateless HTTP implementation.
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"}
@@ -0,0 +1,2 @@
1
+ export { Sessions, Sessions as default } from '@abloatai/transaction/sessions';
2
+ //# sourceMappingURL=sessions.js.map
@@ -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"}
@@ -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 default WebSocket transport with a minted user/session token.
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, authToken, transport: "http" })` because the project
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
- An agent is a **reactive** participant: it wakes on something happening, reads
25
- what it needs, writes a result, and goes idle. That's a request/response
26
- workload so agents talk to Ablo over **plain HTTP**, holding no WebSocket. The
27
- credential *is* the identity; the server resolves the org, scope, and actor from
28
- the key on every request.
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
- Agents get the stateless plane (HTTP). People — when you add the `humans()`
31
- plugin — get the live plane (WebSocket: presence, optimistic, sub-100ms).
32
- **Both operate on the same typed, coordinated state — and coordinate *with each
33
- other*.**
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 pass `transport: 'http'`. No
44
- socket, no connection state just your schema (for types) and an API key.
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, transport: "http" });
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. Those need a WebSocket, so with `transport: 'http'`
66
- they are compile errors. `context().onChange` is separate: while its listener
67
- is active, it holds one HTTP response open until the context changes or the
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
- ## Managed scoped agents
114
+ ## Scoped agent sessions
71
115
 
72
- When this process owns the secret client and also runs the agent, prefer
73
- `agents.create`. It mints the restricted credential, returns a schema-typed
74
- client, and renews that credential for a long run. `sessions.create({ agent })`
75
- is the raw-token path for handing identity to another runtime.
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 agent = await control.agents.create({
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
- syncGroups: [`workspace:${run.workspaceId}`],
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. For independent concurrent
100
- work, omit `id` and let Ablo create distinct identities.
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 managed client per stable scope, but the
103
- cache owns lifecycle: evict idle clients, call `dispose()` on eviction, and
104
- dispose every client during graceful shutdown. Never cache a client and later
105
- reuse it for a different workspace or capability set.
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
- Awaited<ReturnType<typeof control.agents.create>> | undefined
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 created = await control.agents.create({
118
- id: `worker:${key}`,
164
+ const agentId = `worker:${key}`;
165
+ const session = () => sessions.create({
166
+ agent: { id: agentId },
119
167
  can: { records: ['read', 'update'] },
120
- syncGroups: [`workspace:${run.workspaceId}`],
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 `authEndpoint` and mints it. |
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's **one field `apiKey`** and what you pass depends on **where the code runs**.
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** | `authEndpoint`: the route on your own backend that mints a short-lived per-user token | `Ablo({ schema, authEndpoint: '/api/ablo-session' })` |
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
- That's the whole story: one knob, filled by audience.
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 `apiKey` function fetches it. You don't manage refresh — the SDK calls the
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` or `as` option `apiKey` (the key a server holds)
115
- and `authEndpoint` (the mint route a browser points at) are the two credential
116
- knobs, and you set exactly one.
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 server.sessions.create({ user: { id }, can: { records: ['read'] } })` | `ek_` (scoped to `can`) |
123
- | Ready agent client | `await server.agents.create({ can: { records: ['update'] } })` | Auto-refreshing client scoped to `can` |
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
 
@@ -28,10 +28,12 @@ const ablo = Ablo({
28
28
  });
29
29
  ```
30
30
 
31
- The package-root export is the stateless HTTP client for agents, workers, route
32
- handlers, and other server operations. See [Options](./options.md) for its exact
33
- constructor reference. Live state and local reads are added through the
34
- [React client](./react.md).
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
@@ -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
- const { token } = await server.sessions.create({
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, apiKey: token });
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 { credentialEndpointSuccessSchema } from '@abloatai/ablo/auth';
51
- import { ablo } from '@/ablo/server';
50
+ import { sessions } from '@/ablo/sessions';
52
51
 
53
- export async function POST() {
54
- const member = await requireSignedInMember();
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
- syncGroups: [syncGroup('customer', member.customerId)],
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
- syncGroups: [syncGroup('customer', member.customerId)]
122
+ groups: [syncGroup('customer', member.customerId)]
134
123
  ```
135
124
 
136
125
  Resolve `member.customerId` from the membership you just authenticated on the
@@ -120,15 +120,17 @@ is genuinely unavailable.
120
120
 
121
121
  ## 2. The credential each runtime holds
122
122
 
123
- There is one field, `apiKey`, and what goes in it follows from where the code
124
- runs. In production that resolves to four rows:
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`, with `transport: 'http'` | Stateless request/response; nothing held open across invocations. |
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 | `authEndpoint` | A route on your backend mints a short-lived `ek_` per 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 an `authEndpoint`, not a secret key.
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 — `authEndpoint` points at the session route
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
- authEndpoint: '/api/ablo-session',
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 { ablo } from '@/ablo';
90
- import { credentialEndpointSuccessSchema } from '@abloatai/ablo/auth';
95
+ import { sessions } from '@/ablo';
91
96
 
92
97
  export const runtime = 'nodejs';
93
98
 
94
- export async function POST() {
95
- const userId = await currentUserId(); // your auth
96
- const { token, expiresAt } = await ablo.sessions.create({
97
- user: { id: userId },
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
- return Response.json(
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