@abloatai/ablo 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +72 -25
- package/dist/Model.d.ts +39 -0
- package/dist/Model.js +68 -0
- package/dist/auth/credentialPolicy.d.ts +145 -0
- package/dist/auth/credentialPolicy.js +130 -0
- package/dist/cli.cjs +154 -25
- package/dist/client/Ablo.d.ts +39 -88
- package/dist/client/Ablo.js +54 -99
- package/dist/client/ApiClient.d.ts +10 -1
- package/dist/client/ApiClient.js +23 -12
- package/dist/client/auth.d.ts +21 -9
- package/dist/client/auth.js +42 -6
- package/dist/client/createModelProxy.d.ts +74 -10
- package/dist/client/createModelProxy.js +85 -4
- package/dist/client/httpClient.d.ts +17 -3
- package/dist/client/httpClient.js +1 -0
- package/dist/client/identity.js +134 -122
- package/dist/client/index.d.ts +1 -1
- package/dist/client/sessionMint.d.ts +15 -0
- package/dist/client/sessionMint.js +86 -0
- package/dist/errorCodes.d.ts +2 -0
- package/dist/errorCodes.js +3 -1
- package/dist/errors.d.ts +3 -2
- package/dist/errors.js +3 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -7
- package/dist/mutators/RecordingTransaction.js +14 -42
- package/dist/react/AbloProvider.d.ts +1 -6
- package/dist/react/AbloProvider.js +1 -5
- package/dist/react/context.d.ts +1 -31
- package/dist/react/context.js +2 -2
- package/dist/react/index.d.ts +0 -6
- package/dist/react/index.js +0 -7
- package/dist/react/useSyncStatus.d.ts +1 -1
- package/dist/realtime/index.d.ts +1 -1
- package/dist/schema/generate.js +1 -2
- package/dist/schema/schema.d.ts +16 -5
- package/dist/schema/schema.js +26 -0
- package/dist/surface.d.ts +29 -0
- package/dist/surface.js +60 -0
- package/dist/sync/ConnectionManager.d.ts +16 -5
- package/dist/sync/ConnectionManager.js +42 -7
- package/dist/transactions/TransactionQueue.js +22 -10
- package/dist/types/global.d.ts +11 -3
- package/dist/types/global.js +8 -3
- package/dist/types/streams.d.ts +0 -22
- package/dist/utils/mobx-setup.js +1 -0
- package/docs/api-keys.md +49 -0
- package/docs/api.md +6 -5
- package/docs/client-behavior.md +7 -3
- package/docs/coordination.md +88 -24
- package/docs/data-sources.md +29 -9
- package/docs/examples/existing-python-backend.md +9 -5
- package/docs/examples/scoped-agent.md +1 -1
- package/docs/guarantees.md +4 -3
- package/docs/identity.md +89 -82
- package/docs/integration-guide.md +19 -10
- package/docs/migration.md +49 -2
- package/docs/quickstart.md +65 -33
- package/docs/react.md +49 -3
- package/docs/schema-contract.md +23 -5
- package/llms-full.txt +43 -24
- package/llms.txt +17 -15
- package/package.json +1 -1
- package/dist/api/index.d.ts +0 -10
- package/dist/api/index.js +0 -9
- package/dist/principal.d.ts +0 -44
- package/dist/principal.js +0 -49
- package/dist/react/SyncGroupProvider.d.ts +0 -19
- package/dist/react/SyncGroupProvider.js +0 -44
- package/dist/react/useClaim.d.ts +0 -29
- package/dist/react/useClaim.js +0 -42
- package/dist/react/usePresence.d.ts +0 -32
- package/dist/react/usePresence.js +0 -41
package/docs/react.md
CHANGED
|
@@ -31,7 +31,7 @@ import { schema } from '@/ablo/schema';
|
|
|
31
31
|
// from your own server route (see Identity below).
|
|
32
32
|
export const ablo = Ablo({
|
|
33
33
|
schema,
|
|
34
|
-
|
|
34
|
+
apiKey: () => fetch('/api/ablo-session').then((r) => r.text()),
|
|
35
35
|
});
|
|
36
36
|
```
|
|
37
37
|
|
|
@@ -65,13 +65,13 @@ export function Providers({
|
|
|
65
65
|
|
|
66
66
|
| Prop | Default | Purpose |
|
|
67
67
|
| ----------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
|
|
68
|
-
| `client` | — | **Required.** The `Ablo({ schema,
|
|
68
|
+
| `client` | — | **Required.** The `Ablo({ schema, apiKey })` instance. It carries the schema and connection config. |
|
|
69
69
|
| `userId` | resolved from auth | App participant id for app-owned fields and your `identityRoles`. Not the security boundary. |
|
|
70
70
|
| `fallback` | neutral spinner | Rendered during the *first* bootstrap only. Pass a branded skeleton, `null`, or `'passthrough'`. |
|
|
71
71
|
| `onError` | — | Engine / WebSocket / bootstrap errors. Wire to Sentry / Datadog. |
|
|
72
72
|
|
|
73
73
|
Everything that used to be a provider prop — `schema`, `url`, `apiKey`,
|
|
74
|
-
`teamIds`, `syncGroups
|
|
74
|
+
`teamIds`, `syncGroups`, `persistence`, `bootstrapMode` — now lives on
|
|
75
75
|
the `Ablo({ ... })` client you build before mounting the provider. Where the
|
|
76
76
|
identity comes from, and why the API key never reaches the browser, is the whole
|
|
77
77
|
of [Identity & Sync Groups](./identity.md) — read that if it isn't obvious how
|
|
@@ -178,6 +178,52 @@ imperative work after an event or effect.
|
|
|
178
178
|
|
|
179
179
|
See [API reference](/docs/api) for the full options surface.
|
|
180
180
|
|
|
181
|
+
## useClaim — named-claim dispatcher
|
|
182
|
+
|
|
183
|
+
`useClaim` (renamed from `useIntent` in 0.11.0) is typed sugar for invoking a
|
|
184
|
+
*named* claim from your own coordination vocabulary — distinct from the
|
|
185
|
+
row-level `ablo.<model>.claim({ id })` resource claim. Use it when you want to
|
|
186
|
+
broadcast a semantic claim like "I'm editing this layer" or "the agent is
|
|
187
|
+
generating here" and let your transport turn it into a network effect.
|
|
188
|
+
|
|
189
|
+
Declare the vocabulary once via module augmentation on the `Register` interface
|
|
190
|
+
(the `Claims` key — previously `Intents`):
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
declare module '@abloatai/ablo' {
|
|
194
|
+
interface Register {
|
|
195
|
+
Claims: {
|
|
196
|
+
editLayer: { slideId: string; layerId: string };
|
|
197
|
+
generateWithAI: { entityId: string; tool: string };
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Then `useClaim('editLayer')` returns a function whose sole argument is the
|
|
204
|
+
`editLayer` shape — purely compile-time narrowing, no runtime checks:
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
'use client';
|
|
208
|
+
|
|
209
|
+
import { useClaim } from '@abloatai/ablo/react';
|
|
210
|
+
|
|
211
|
+
export function LayerToolbar({ slideId, layerId }: { slideId: string; layerId: string }) {
|
|
212
|
+
const claimEditLayer = useClaim('editLayer');
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<button onClick={() => claimEditLayer({ slideId, layerId })}>
|
|
216
|
+
Edit layer
|
|
217
|
+
</button>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The hook is pure sugar: the actual network effect lives in the `beginClaim`
|
|
223
|
+
function wired into the provider (bound to your transport). If no `beginClaim`
|
|
224
|
+
is wired, the returned invoker throws `AbloValidationError` with code
|
|
225
|
+
`claim_not_wired`.
|
|
226
|
+
|
|
181
227
|
## Next.js
|
|
182
228
|
|
|
183
229
|
The Next.js [App Router landing](/nextjs) walks through Server Components
|
package/docs/schema-contract.md
CHANGED
|
@@ -49,6 +49,20 @@ The model key (`weatherReports`) becomes the client namespace
|
|
|
49
49
|
contract. You should not create a parallel string-keyed write path for the same
|
|
50
50
|
data.
|
|
51
51
|
|
|
52
|
+
### Reserved fields
|
|
53
|
+
|
|
54
|
+
The SDK provides these on every row automatically — do **not** declare them in
|
|
55
|
+
your `model(...)` fields:
|
|
56
|
+
|
|
57
|
+
- `id`
|
|
58
|
+
- `createdAt`
|
|
59
|
+
- `updatedAt`
|
|
60
|
+
- `organizationId`
|
|
61
|
+
- `createdBy`
|
|
62
|
+
|
|
63
|
+
Declare only your own fields; the reserved ones are still present on the row and
|
|
64
|
+
readable, you just don't author them.
|
|
65
|
+
|
|
52
66
|
## Reads and writes
|
|
53
67
|
|
|
54
68
|
Use async reads when the row may not be local:
|
|
@@ -88,12 +102,16 @@ the fresh row. Reads stay open; only acting on the row serializes.
|
|
|
88
102
|
|
|
89
103
|
## Storage boundary
|
|
90
104
|
|
|
91
|
-
Every schema model is backed by your own database
|
|
92
|
-
|
|
105
|
+
Every schema model is backed by your own database. There are three start states,
|
|
106
|
+
all covered in [Connect Your Database](./data-sources.md) (the single source of
|
|
107
|
+
truth): the sandbox (`apiKey` only, no database), a direct connection string
|
|
108
|
+
(`databaseUrl` passed to `Ablo(...)`, a live, server-only option), or a signed
|
|
109
|
+
Data Source endpoint where your app keeps the database credential and commits
|
|
110
|
+
each write itself.
|
|
93
111
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
API key.
|
|
112
|
+
If your database stays canonical behind a Data Source endpoint, do not pass
|
|
113
|
+
`databaseUrl` to `Ablo(...)` — trusted runtimes use `ABLO_API_KEY`. Browser code
|
|
114
|
+
goes through `<AbloProvider>` or a scoped session route, never a raw API key.
|
|
97
115
|
|
|
98
116
|
## Rules of thumb
|
|
99
117
|
|
package/llms-full.txt
CHANGED
|
@@ -19,7 +19,7 @@ Public imports:
|
|
|
19
19
|
- `@abloatai/ablo/react` — React provider and hooks.
|
|
20
20
|
- `@abloatai/ablo/testing` — test harnesses and mocks.
|
|
21
21
|
|
|
22
|
-
TYPES:
|
|
22
|
+
TYPES: the project registers its schema ONCE via declaration merging — `npx ablo init` scaffolds `ablo/register.ts` (a regular `.ts` module beside schema.ts, NOT a hand-authored `.d.ts`): `import type { schema } from './schema'; declare module '@abloatai/ablo' { interface Register { Schema: typeof schema } }`. The top-level `import type` makes `declare module` MERGE (augment) the SDK's Register interface rather than collide — same shape TanStack Router uses in src/router.tsx; any `.ts` file in tsconfig include works, never imported. Then model types are one parameter: `type Task = Model<'tasks'>` (import type { Model } from '@abloatai/ablo/schema'). Do NOT teach `InferModel` (deprecated) or the two-param `Model<typeof schema,'tasks'>` unless multiple schemas exist. Never hand-write model interfaces — derive from the schema. To NAME the client type (function param, context value), infer from the value: `type Sync = typeof sync` — same idiom as tRPC `typeof appRouter` / Drizzle `typeof db`; it resolves the typed overload at the call site. Do NOT use `ReturnType<typeof Ablo>` (collapses to the untyped last overload) and do NOT import a bespoke client-type generic — there is none.
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, or `internal/*` as public imports. The Data Source surface — `/source`, `/source/next`, `/source/drizzle`, `/source/kysely`, `/source/conformance` — IS public (it's how a customer-owned database is wired).
|
|
@@ -39,9 +39,9 @@ Use `snapshot(...)` and `readAt` when an agent write depends on state it already
|
|
|
39
39
|
read. `onStale: 'reject'` prevents lost updates by rejecting if the target
|
|
40
40
|
changed after the snapshot.
|
|
41
41
|
|
|
42
|
-
Claims are live coordination signals, not database locks.
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
Claims are live coordination signals, not database locks. Reads never block on a
|
|
43
|
+
claim — to wait for a row to free up, `claim({ id })` it and the claim queues
|
|
44
|
+
fairly behind the current holder.
|
|
45
45
|
|
|
46
46
|
Agent run bookkeeping is internal. Most users should not create run ledger
|
|
47
47
|
records or scoped access credentials manually.
|
|
@@ -145,9 +145,21 @@ This applies to any API-backed app: Python, Rails, Go, or Node. The backend keep
|
|
|
145
145
|
its service layer and DB credentials. Ablo gets a Data Source endpoint and uses
|
|
146
146
|
`ABLO_API_KEY`, not a database URL.
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
`
|
|
150
|
-
|
|
148
|
+
The connection-string path is the PRIMARY one: pass `databaseUrl` explicitly to
|
|
149
|
+
`Ablo({ databaseUrl })`. It is NOT auto-read from the environment — a
|
|
150
|
+
`DATABASE_URL` set for another tool (Prisma, Drizzle, docker-compose) is ignored
|
|
151
|
+
unless you pass `databaseUrl`. The typical user ALREADY has a Postgres (often
|
|
152
|
+
Prisma-managed for auth/audit/log tables that are NOT in the Ablo schema); Ablo
|
|
153
|
+
syncs a SUBSET of models against it. Most users do NOT run `ablo migrate` — they
|
|
154
|
+
ADOPT existing tables with `npx ablo pull` / `npx ablo check`, or keep managing
|
|
155
|
+
tables with their own migration tool. Run `npx ablo migrate` only when Ablo
|
|
156
|
+
should OWN the tables. NOTE: Ablo's CLOUD connects to your Postgres over the
|
|
157
|
+
NETWORK, so a localhost / private-range DB is unreachable and rejected — for a
|
|
158
|
+
local dev DB, expose a signed Data Source endpoint (your app proxies to it) or
|
|
159
|
+
use the hosted sandbox (no DB needed).
|
|
160
|
+
|
|
161
|
+
When the user DOES use the connection-string path: the role must be NON-superuser
|
|
162
|
+
and NON-BYPASSRLS — Ablo enforces row-level security and rejects owner roles with
|
|
151
163
|
`database_role_cannot_enforce_rls`. Neon's and Supabase's default dashboard
|
|
152
164
|
strings use the database OWNER (e.g. `neondb_owner`) and ARE rejected. EASIEST: have the user run `npx ablo migrate` — it detects the unsafe role and creates the scoped one automatically from their machine (owner credential never reaches Ablo; new DATABASE_URL written to the env file). Manual alternative — create a scoped role first (`CREATE ROLE ablo_app LOGIN PASSWORD '...'
|
|
153
165
|
NOSUPERUSER NOBYPASSRLS; GRANT CREATE, CONNECT ON DATABASE <db> TO ablo_app;
|
|
@@ -162,9 +174,11 @@ The options that matter: `schema` and `apiKey`. Everything else
|
|
|
162
174
|
breaks the connection. It exists only for self-hosted/proxy setups the human
|
|
163
175
|
explicitly asks for.
|
|
164
176
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
177
|
+
`databaseUrl` is an OPTIONAL, server-only option on `Ablo(...)`. It is NOT
|
|
178
|
+
auto-read from the environment — pass it EXPLICITLY to register your Postgres
|
|
179
|
+
directly (the primary connection-string path). Omit it when you expose a signed
|
|
180
|
+
Data Source endpoint (so customer-owned app databases stay private), or when
|
|
181
|
+
trying Ablo against the hosted sandbox (apiKey only, no database).
|
|
168
182
|
|
|
169
183
|
Important per-write options: `wait`, `readAt`, `onStale`,
|
|
170
184
|
`idempotencyKey`, and `timeout`.
|
|
@@ -187,7 +201,10 @@ There are two sandbox surfaces:
|
|
|
187
201
|
be pasted into Claude Code or Codex to wire one real model through Ablo.
|
|
188
202
|
- Authenticated org sandboxes are real test environments. The default sandbox is
|
|
189
203
|
the Stripe-style sandbox for an org. It has an isolated sync group prefix,
|
|
190
|
-
can mint `sk_test_*` keys, and can be reset without touching live state.
|
|
204
|
+
can mint `sk_test_*` keys, and can be reset without touching live state. The
|
|
205
|
+
sandbox CAN host rows in Ablo's test plane, so you can try Ablo with NO
|
|
206
|
+
database — `apiKey` only, no `databaseUrl` — like Stripe test mode. (In
|
|
207
|
+
production, your own Postgres is the system of record.)
|
|
191
208
|
|
|
192
209
|
Additional org sandboxes can start blank or copy live configuration. Keep
|
|
193
210
|
customer production traffic on `sk_live_*`; use `sk_test_*` for app setup, Data
|
|
@@ -290,7 +307,10 @@ common agent path.
|
|
|
290
307
|
|
|
291
308
|
## Model
|
|
292
309
|
|
|
293
|
-
|
|
310
|
+
The read shape depends on whether the client has a local graph:
|
|
311
|
+
|
|
312
|
+
- The stateful `ablo.<model>.retrieve({ id })` returns the BARE row (`T | undefined`), and `list({ where })` returns `T[]`. Read coordination metadata (state watermark, active claims) separately via `ablo.snapshot(...)` and `ablo.<model>.claim.state({ id })`.
|
|
313
|
+
- The stateless HTTP / `.model(name)` `retrieve({ id })` returns a `ModelRead<T>` envelope because it has no local graph to carry the watermark; `list({ where })` still returns `T[]`.
|
|
294
314
|
|
|
295
315
|
```ts
|
|
296
316
|
type ModelRead<T> = {
|
|
@@ -300,32 +320,31 @@ type ModelRead<T> = {
|
|
|
300
320
|
};
|
|
301
321
|
```
|
|
302
322
|
|
|
303
|
-
`stamp` is the state watermark. Pass it to writes as `readAt
|
|
323
|
+
`stamp` is the state watermark. Pass it to writes as `readAt` (from `ablo.snapshot(...)` on the stateful client, or from the envelope on the HTTP client).
|
|
304
324
|
|
|
305
325
|
`claims` lists active work on the target. Reads are allowed while another participant is working. The caller decides whether to return, fail, or wait.
|
|
306
326
|
|
|
307
327
|
## Claimed Behavior
|
|
308
328
|
|
|
309
|
-
Claimed behavior is explicit:
|
|
329
|
+
Claimed behavior is explicit, and there are only two policies:
|
|
310
330
|
|
|
311
|
-
- `ifClaimed: 'return'` returns the current claims with the read.
|
|
331
|
+
- `ifClaimed: 'return'` (the default) returns the row plus the current claims with the read.
|
|
312
332
|
- `ifClaimed: 'fail'` throws `AbloClaimedError`.
|
|
313
|
-
- `ifClaimed: 'wait'` waits for matching claims to clear.
|
|
314
|
-
|
|
315
|
-
Schema clients use the realtime claim stream for waits.
|
|
316
333
|
|
|
317
|
-
|
|
334
|
+
Reads never block on a claim. To wait for a row to free up, `claim({ id })` it —
|
|
335
|
+
the claim queues fairly behind the current holder and is granted when the row
|
|
336
|
+
frees. Use `ifClaimed: 'fail'` when you'd rather refuse to read a claimed row.
|
|
318
337
|
|
|
319
338
|
```ts
|
|
339
|
+
// Read a claimed row without blocking, surfacing who holds it:
|
|
320
340
|
await api.model('reports').retrieve({
|
|
321
341
|
id: 'report_stockholm',
|
|
322
|
-
ifClaimed: '
|
|
323
|
-
claimedPollInterval: 1_000,
|
|
324
|
-
claimedTimeout: 30_000,
|
|
342
|
+
ifClaimed: 'return',
|
|
325
343
|
});
|
|
326
|
-
```
|
|
327
344
|
|
|
328
|
-
|
|
345
|
+
// Or wait for it to free up by queueing your own claim:
|
|
346
|
+
await api.model('reports').claim({ id: 'report_stockholm' });
|
|
347
|
+
```
|
|
329
348
|
|
|
330
349
|
## Write
|
|
331
350
|
|
package/llms.txt
CHANGED
|
@@ -8,7 +8,7 @@ Use AI SDK for the agent loop. Use Ablo when agent reads and writes must persist
|
|
|
8
8
|
|
|
9
9
|
## Start here
|
|
10
10
|
|
|
11
|
-
First action when integrating into an app: run `npx ablo init --yes --framework <nextjs|vite|remix|vanilla
|
|
11
|
+
First action when integrating into an app: run `npx ablo init --yes --framework <nextjs|vite|remix|vanilla>`. Agents have no TTY — `--yes` is REQUIRED or it HANGS. The default `--storage direct` (connection string) scaffolds `ablo/schema.ts`, the client, and (for Next.js) the browser provider + session route; pass `--storage endpoint` instead when database credentials must stay inside the app — that variant scaffolds a signed Data Source endpoint. All on the current API. Edit the generated files rather than hand-writing from this doc.
|
|
12
12
|
|
|
13
13
|
Second: make sure a key exists — WITHOUT printing it. The key is a secret; it must never appear in your output, your reasoning, or a file you echo (it would live in the conversation history forever). Check PRESENCE only: `[ -n "$ABLO_API_KEY" ] && echo set` and `grep -cq '^ABLO_API_KEY=' .env.local && echo wired` — never `cat .env.local`, never `echo $ABLO_API_KEY`. If neither check passes, ask the HUMAN to run `npx ablo login` once — it opens a browser and saves a `sk_test_` key locally; an agent must NOT run it. You never copy the key by hand: the next step writes it into `.env.local` (and gitignores it) for you.
|
|
14
14
|
|
|
@@ -24,7 +24,7 @@ Each app gets its own PROJECT inside the org — its own schema, its own sandbox
|
|
|
24
24
|
import Ablo from '@abloatai/ablo';
|
|
25
25
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
26
26
|
|
|
27
|
-
TYPES:
|
|
27
|
+
TYPES: the project registers its schema ONCE via declaration merging — `npx ablo init` scaffolds `ablo/register.ts` (a regular `.ts` module beside schema.ts, NOT a hand-authored `.d.ts`): `import type { schema } from './schema'; declare module '@abloatai/ablo' { interface Register { Schema: typeof schema } }`. The top-level `import type` makes `declare module` MERGE (augment) the SDK's Register interface rather than collide — same shape TanStack Router uses in src/router.tsx; any `.ts` file in tsconfig include works, never imported. Then model types are one parameter: `type Task = Model<'tasks'>` (import type { Model } from '@abloatai/ablo/schema'). Do NOT teach `InferModel` (deprecated) or the two-param `Model<typeof schema,'tasks'>` unless multiple schemas exist. Never hand-write model interfaces — derive from the schema. To NAME the client type (function param, context value), infer from the value: `type Sync = typeof sync` — same idiom as tRPC `typeof appRouter` / Drizzle `typeof db`; it resolves the typed overload at the call site. Do NOT use `ReturnType<typeof Ablo>` (collapses to the untyped last overload) and do NOT import a bespoke client-type generic — there is none.
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
const schema = defineSchema({
|
|
@@ -99,13 +99,13 @@ coordination until the app reports it through Data Source events.
|
|
|
99
99
|
## Claimed Behavior
|
|
100
100
|
|
|
101
101
|
Reads never silently block. Schema reads stay open while a row is claimed.
|
|
102
|
-
Server reads through `ablo.model(name)` can pass `ifClaimed: 'return'`
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
Server reads through `ablo.model(name)` can pass `ifClaimed: 'return'` (the
|
|
103
|
+
default — returns the row plus active claim metadata) or `ifClaimed: 'fail'`
|
|
104
|
+
to throw `AbloClaimedError`.
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Use `
|
|
106
|
+
Reads never block on a claim. To wait for a row to free up, `claim({ id })` it —
|
|
107
|
+
the claim queues fairly behind the current holder and is granted when the row
|
|
108
|
+
frees. Use `ifClaimed: 'fail'` when you'd rather refuse to read a claimed row.
|
|
109
109
|
|
|
110
110
|
## Guarantees
|
|
111
111
|
|
|
@@ -123,9 +123,9 @@ A schema is model fields and relations. Advanced schema helpers such as `mutable
|
|
|
123
123
|
|
|
124
124
|
## Storage Boundary
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
`databaseUrl` is an OPTIONAL, server-only constructor option on `Ablo(...)`. It is NOT auto-read from the environment — pass it EXPLICITLY to register your Postgres directly. Omit it when you expose a signed Data Source endpoint, or when trying Ablo against the hosted sandbox (apiKey only). A `DATABASE_URL` set for another tool (Prisma, Drizzle, docker-compose) is ignored unless you pass `databaseUrl` explicitly.
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
In production, every schema model is backed by YOUR OWN database. The PRIMARY path is the connection string: pass `databaseUrl` (most users already have a Postgres — often Prisma- or Drizzle-managed for auth/audit/log tables that are NOT in the Ablo schema; Ablo syncs a SUBSET of models against it). Most users do NOT run `ablo migrate` — they ADOPT existing tables with `npx ablo pull` / `npx ablo check`, or keep managing tables with their own migration tool. Run `npx ablo migrate` only when Ablo should OWN the tables (it provisions the synced-model tables in your DB). The alternative to the connection string is a signed Data Source endpoint that hands Ablo an ORM `adapter` (Drizzle is the default; Prisma and Kysely are also supported) — it owns the transaction, exactly-once idempotency, and outbox in ONE pass (no hand-written `commit`/`events`); use it when database credentials must never leave your infrastructure (or for a local/private-range DB, which Ablo's cloud cannot reach over the network).
|
|
129
129
|
|
|
130
130
|
GOTCHA the user WILL hit: `DATABASE_URL` must use a NON-superuser, NON-BYPASSRLS role (Ablo enforces row-level security; owner roles are rejected with `database_role_cannot_enforce_rls`). Neon's and Supabase's default dashboard connection strings use the database OWNER (e.g. `neondb_owner`) and are rejected. EASIEST: `npx ablo migrate` detects the unsafe role and creates the scoped one automatically from the user's machine (owner credential never reaches Ablo; new DATABASE_URL written to the env file). Manual alternative — create a scoped role first: `CREATE ROLE ablo_app LOGIN PASSWORD '...' NOSUPERUSER NOBYPASSRLS; GRANT CREATE, CONNECT ON DATABASE <db> TO ablo_app; GRANT CREATE, USAGE ON SCHEMA public TO ablo_app;` — then swap user/password into the same host/db string.
|
|
131
131
|
|
|
@@ -145,7 +145,7 @@ export const { POST } = dataSourceNext({
|
|
|
145
145
|
});
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
`npx ablo init` defaults to
|
|
148
|
+
`npx ablo init` defaults to `--storage direct` (the connection-string path — it scaffolds the client carrying `databaseUrl` and the `DATABASE_URL` env entry; see CLI below). Pass `--storage endpoint` to scaffold the signed Data Source endpoint above instead, when app database credentials must stay private — Ablo only calls the endpoint.
|
|
149
149
|
|
|
150
150
|
## Sandboxes
|
|
151
151
|
|
|
@@ -156,9 +156,11 @@ when an agent is asked to "make Ablo work" in an existing app.
|
|
|
156
156
|
|
|
157
157
|
Authenticated org sandboxes are real test environments. Treat the default
|
|
158
158
|
sandbox like Stripe test mode: it has an isolated sync group prefix and mints
|
|
159
|
-
`sk_test_*` keys.
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
`sk_test_*` keys. The sandbox CAN host rows in Ablo's test plane, so you can try
|
|
160
|
+
Ablo with NO database — `apiKey` only, no `databaseUrl`. (In production, your own
|
|
161
|
+
Postgres is the system of record.) Extra sandboxes can start blank or copy live
|
|
162
|
+
configuration. Resetting a sandbox creates a clean future stream without
|
|
163
|
+
touching live data. Use `sk_live_*` only for production.
|
|
162
164
|
|
|
163
165
|
For coding agents, the sandbox success path is: pick one shared model,
|
|
164
166
|
declare schema, create the Ablo client, replace one direct mutation with a typed
|
|
@@ -185,7 +187,7 @@ Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, or internal subp
|
|
|
185
187
|
|
|
186
188
|
`ablo init` and other prompts need a TTY; an agent/CI run has none and will HANG. Always:
|
|
187
189
|
|
|
188
|
-
- `npx ablo init --yes` (flags: `--framework`, `--auth`, `--storage
|
|
190
|
+
- `npx ablo init --yes` (flags: `--framework`, `--auth`, `--storage direct|endpoint` (default `direct`), `--no-agent`, `--no-pull`, `--no-install`, `--no-login`). Generates `ablo/schema.ts` + the client; `--storage direct` (default) wires `databaseUrl`, `--storage endpoint` scaffolds the `ablo/data-source.ts` endpoint above instead.
|
|
189
191
|
- Key: see "Start here" — env → `.env.local` → ask the human to `npx ablo login`; never run `login` yourself, never copy keys by hand (`ablo push` writes `.env.local`).
|
|
190
192
|
- Adopt an existing DB: `npx ablo pull prisma [path]` / `npx ablo pull drizzle <module>`.
|
|
191
193
|
- `npx ablo push --no-watch` pushes the schema (sandbox) AND writes `ABLO_API_KEY` to `.env.local` (default watches forever); `npx ablo logs --no-follow` (default tails forever); `npx ablo mode sandbox|production` (always pass the arg). `npx ablo push`/`status`/`pull`/`check`/`generate` are one-shot.
|
package/package.json
CHANGED
package/dist/api/index.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal compatibility entrypoint for the stateless hosted protocol client.
|
|
3
|
-
*
|
|
4
|
-
* Use this build for serverless functions, scripts, and backends that want
|
|
5
|
-
* model reads/writes and commits over HTTP without the realtime sync runtime.
|
|
6
|
-
*/
|
|
7
|
-
export { createProtocolClient, createProtocolClient as Ablo, type AbloApi, type AbloApiClientOptions, type AbloApiClaims, type Capability, type CapabilityCreateOptions, type CapabilityParticipantKind, type CapabilityRecord, type CapabilityResource, type CapabilityRevocation, type CapabilityScope, } from '../client/ApiClient.js';
|
|
8
|
-
export type { CommitCreateOptions, CommitOperationInput, CommitReceipt, CommitWait, ClaimCreateOptions, ClaimHandle, ClaimWaitOptions, ClaimedOptions, IfClaimedPolicy, ModelClient, ModelClaim, ModelMutationOptions, ModelReadOptions, ModelRead, ModelTarget, } from '../client/Ablo.js';
|
|
9
|
-
import { createProtocolClient } from '../client/ApiClient.js';
|
|
10
|
-
export default createProtocolClient;
|
package/dist/api/index.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Internal compatibility entrypoint for the stateless hosted protocol client.
|
|
3
|
-
*
|
|
4
|
-
* Use this build for serverless functions, scripts, and backends that want
|
|
5
|
-
* model reads/writes and commits over HTTP without the realtime sync runtime.
|
|
6
|
-
*/
|
|
7
|
-
export { createProtocolClient, createProtocolClient as Ablo, } from '../client/ApiClient.js';
|
|
8
|
-
import { createProtocolClient } from '../client/ApiClient.js';
|
|
9
|
-
export default createProtocolClient;
|
package/dist/principal.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Principal constructors — a thin typed façade over the raw
|
|
3
|
-
* `SessionRef` / `AgentRef` shapes so call sites don't have to memorize
|
|
4
|
-
* the discriminated-union tags.
|
|
5
|
-
*
|
|
6
|
-
* ```ts
|
|
7
|
-
* import Ablo, { session } from '@abloatai/ablo';
|
|
8
|
-
*
|
|
9
|
-
* const ablo = Ablo({ schema, apiKey });
|
|
10
|
-
* const participant = await ablo.participants.join({
|
|
11
|
-
* type: 'Matter',
|
|
12
|
-
* id: 'deal-1',
|
|
13
|
-
* });
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* Browser-human flows use `session(...)`. Agent-spawn-agent flows use
|
|
17
|
-
* `agent(...)`, but those rarely appear in customer code because the
|
|
18
|
-
* participant layer handles attenuation.
|
|
19
|
-
*
|
|
20
|
-
* These are pure — no I/O, no hidden state. If the shape ever grows a
|
|
21
|
-
* required field (say, a scope hint for the restricted key), the helper
|
|
22
|
-
* is the one place to flag migrations.
|
|
23
|
-
*/
|
|
24
|
-
import type { AgentRef, SessionRef } from './types/streams.js';
|
|
25
|
-
/**
|
|
26
|
-
* Build a `SessionRef` from the identifiers your auth system already
|
|
27
|
-
* holds. Typical inputs: the Better Auth session id, the user id, and
|
|
28
|
-
* the organization the session is scoped to.
|
|
29
|
-
*/
|
|
30
|
-
export declare function session(params: {
|
|
31
|
-
id: string;
|
|
32
|
-
userId: string;
|
|
33
|
-
organizationId: string;
|
|
34
|
-
}): SessionRef;
|
|
35
|
-
/**
|
|
36
|
-
* Build an `AgentRef` from an agent id + the capability token that
|
|
37
|
-
* authenticates it. Rare in application code — the common path is
|
|
38
|
-
* `participant.join(child)` where the parent's token is attenuated
|
|
39
|
-
* automatically.
|
|
40
|
-
*/
|
|
41
|
-
export declare function agent(params: {
|
|
42
|
-
id: string;
|
|
43
|
-
capabilityToken: string;
|
|
44
|
-
}): AgentRef;
|
package/dist/principal.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Principal constructors — a thin typed façade over the raw
|
|
3
|
-
* `SessionRef` / `AgentRef` shapes so call sites don't have to memorize
|
|
4
|
-
* the discriminated-union tags.
|
|
5
|
-
*
|
|
6
|
-
* ```ts
|
|
7
|
-
* import Ablo, { session } from '@abloatai/ablo';
|
|
8
|
-
*
|
|
9
|
-
* const ablo = Ablo({ schema, apiKey });
|
|
10
|
-
* const participant = await ablo.participants.join({
|
|
11
|
-
* type: 'Matter',
|
|
12
|
-
* id: 'deal-1',
|
|
13
|
-
* });
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* Browser-human flows use `session(...)`. Agent-spawn-agent flows use
|
|
17
|
-
* `agent(...)`, but those rarely appear in customer code because the
|
|
18
|
-
* participant layer handles attenuation.
|
|
19
|
-
*
|
|
20
|
-
* These are pure — no I/O, no hidden state. If the shape ever grows a
|
|
21
|
-
* required field (say, a scope hint for the restricted key), the helper
|
|
22
|
-
* is the one place to flag migrations.
|
|
23
|
-
*/
|
|
24
|
-
/**
|
|
25
|
-
* Build a `SessionRef` from the identifiers your auth system already
|
|
26
|
-
* holds. Typical inputs: the Better Auth session id, the user id, and
|
|
27
|
-
* the organization the session is scoped to.
|
|
28
|
-
*/
|
|
29
|
-
export function session(params) {
|
|
30
|
-
return {
|
|
31
|
-
kind: 'session',
|
|
32
|
-
id: params.id,
|
|
33
|
-
userId: params.userId,
|
|
34
|
-
organizationId: params.organizationId,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Build an `AgentRef` from an agent id + the capability token that
|
|
39
|
-
* authenticates it. Rare in application code — the common path is
|
|
40
|
-
* `participant.join(child)` where the parent's token is attenuated
|
|
41
|
-
* automatically.
|
|
42
|
-
*/
|
|
43
|
-
export function agent(params) {
|
|
44
|
-
return {
|
|
45
|
-
kind: 'agent',
|
|
46
|
-
id: params.id,
|
|
47
|
-
capabilityToken: params.capabilityToken,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
export interface SyncGroupProviderProps {
|
|
3
|
-
/** The sync-group identifier — e.g., `matter:abc-123`, `deck:xyz`. */
|
|
4
|
-
id: string;
|
|
5
|
-
children: ReactNode;
|
|
6
|
-
}
|
|
7
|
-
export declare function SyncGroupProvider({ id, children }: SyncGroupProviderProps): import("react").JSX.Element;
|
|
8
|
-
/**
|
|
9
|
-
* Returns the ID of the nearest `<SyncGroupProvider>`. Throws if
|
|
10
|
-
* called outside one — sync-group awareness is mandatory by design,
|
|
11
|
-
* so the error points the consumer at the provider instead of
|
|
12
|
-
* returning undefined and letting downstream code silently miss scope.
|
|
13
|
-
*
|
|
14
|
-
* If a component legitimately renders both inside and outside a
|
|
15
|
-
* group, structure the tree so the hook is only called on the
|
|
16
|
-
* inside path (e.g., split into two components). Silent nulls are
|
|
17
|
-
* never the right answer.
|
|
18
|
-
*/
|
|
19
|
-
export declare function useSyncGroup(): string;
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useContext, useMemo } from 'react';
|
|
4
|
-
import { AbloValidationError } from '../errors.js';
|
|
5
|
-
/**
|
|
6
|
-
* Narrow context for a per-entity sync-group scope. Maps directly onto
|
|
7
|
-
* Liveblocks' `<RoomProvider id="...">`: wrap a subtree, and any hooks
|
|
8
|
-
* inside can read `useSyncGroup()` to discover "which entity am I
|
|
9
|
-
* scoped to?" without threading the ID through props.
|
|
10
|
-
*
|
|
11
|
-
* Typical IDs follow the multiplayer sync-group convention: `matter:<id>`,
|
|
12
|
-
* `deck:<id>`, `project:<id>`. The ID is an opaque string — the
|
|
13
|
-
* provider doesn't parse it.
|
|
14
|
-
*
|
|
15
|
-
* v0.3.0 scope: this is a thin passthrough. Future versions will
|
|
16
|
-
* scope `useQuery` / `useOne` results to the group automatically.
|
|
17
|
-
*/
|
|
18
|
-
const SyncGroupContext = createContext(null);
|
|
19
|
-
export function SyncGroupProvider({ id, children }) {
|
|
20
|
-
// Stabilize the context value so consumers memoized on it don't
|
|
21
|
-
// re-render when the provider re-renders for unrelated reasons.
|
|
22
|
-
const value = useMemo(() => id, [id]);
|
|
23
|
-
return _jsx(SyncGroupContext.Provider, { value: value, children: children });
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Returns the ID of the nearest `<SyncGroupProvider>`. Throws if
|
|
27
|
-
* called outside one — sync-group awareness is mandatory by design,
|
|
28
|
-
* so the error points the consumer at the provider instead of
|
|
29
|
-
* returning undefined and letting downstream code silently miss scope.
|
|
30
|
-
*
|
|
31
|
-
* If a component legitimately renders both inside and outside a
|
|
32
|
-
* group, structure the tree so the hook is only called on the
|
|
33
|
-
* inside path (e.g., split into two components). Silent nulls are
|
|
34
|
-
* never the right answer.
|
|
35
|
-
*/
|
|
36
|
-
export function useSyncGroup() {
|
|
37
|
-
const id = useContext(SyncGroupContext);
|
|
38
|
-
if (!id) {
|
|
39
|
-
throw new AbloValidationError('useSyncGroup: no <SyncGroupProvider> mounted above this component. ' +
|
|
40
|
-
'Wrap your tree with <SyncGroupProvider id="matter:..."> from ' +
|
|
41
|
-
'@abloatai/ablo/react.', { code: 'no_sync_group_provider' });
|
|
42
|
-
}
|
|
43
|
-
return id;
|
|
44
|
-
}
|
package/dist/react/useClaim.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { ResolveClaims } from '../types/global.js';
|
|
2
|
-
/**
|
|
3
|
-
* Named-claim invoker, typed via `ResolveClaims[ClaimName]`.
|
|
4
|
-
*
|
|
5
|
-
* The consumer declares their claim vocabulary in the global:
|
|
6
|
-
*
|
|
7
|
-
* ```ts
|
|
8
|
-
* declare module '@abloatai/ablo' {
|
|
9
|
-
* interface Register {
|
|
10
|
-
* Claims: {
|
|
11
|
-
* editLayer: { slideId: string; layerId: string };
|
|
12
|
-
* generateWithAI: { entityId: string; tool: string };
|
|
13
|
-
* };
|
|
14
|
-
* }
|
|
15
|
-
* }
|
|
16
|
-
* ```
|
|
17
|
-
*
|
|
18
|
-
* Then `useClaim('editLayer')` returns a function whose sole argument
|
|
19
|
-
* is the `editLayer` claim shape — no runtime checks, purely compile-
|
|
20
|
-
* time narrowing.
|
|
21
|
-
*
|
|
22
|
-
* The SDK doesn't own what happens next: the `beginClaim` function on
|
|
23
|
-
* the React context (supplied via `SyncProvider`) is where the claim
|
|
24
|
-
* claim turns into a network effect. A Node-backed consumer wires it
|
|
25
|
-
* through `SyncAgent.beginClaim`; a browser-backed consumer may
|
|
26
|
-
* broadcast it through their own WebSocket. This hook is pure sugar
|
|
27
|
-
* that adds the typed name + claim narrowing.
|
|
28
|
-
*/
|
|
29
|
-
export declare function useClaim<Name extends keyof ResolveClaims & string>(claimName: Name): (claim: ResolveClaims[Name]) => unknown;
|
package/dist/react/useClaim.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { useCallback } from 'react';
|
|
3
|
-
import { useSyncContext } from './context.js';
|
|
4
|
-
import { AbloValidationError } from '../errors.js';
|
|
5
|
-
/**
|
|
6
|
-
* Named-claim invoker, typed via `ResolveClaims[ClaimName]`.
|
|
7
|
-
*
|
|
8
|
-
* The consumer declares their claim vocabulary in the global:
|
|
9
|
-
*
|
|
10
|
-
* ```ts
|
|
11
|
-
* declare module '@abloatai/ablo' {
|
|
12
|
-
* interface Register {
|
|
13
|
-
* Claims: {
|
|
14
|
-
* editLayer: { slideId: string; layerId: string };
|
|
15
|
-
* generateWithAI: { entityId: string; tool: string };
|
|
16
|
-
* };
|
|
17
|
-
* }
|
|
18
|
-
* }
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* Then `useClaim('editLayer')` returns a function whose sole argument
|
|
22
|
-
* is the `editLayer` claim shape — no runtime checks, purely compile-
|
|
23
|
-
* time narrowing.
|
|
24
|
-
*
|
|
25
|
-
* The SDK doesn't own what happens next: the `beginClaim` function on
|
|
26
|
-
* the React context (supplied via `SyncProvider`) is where the claim
|
|
27
|
-
* claim turns into a network effect. A Node-backed consumer wires it
|
|
28
|
-
* through `SyncAgent.beginClaim`; a browser-backed consumer may
|
|
29
|
-
* broadcast it through their own WebSocket. This hook is pure sugar
|
|
30
|
-
* that adds the typed name + claim narrowing.
|
|
31
|
-
*/
|
|
32
|
-
export function useClaim(claimName) {
|
|
33
|
-
const { beginClaim } = useSyncContext();
|
|
34
|
-
return useCallback((claim) => {
|
|
35
|
-
if (!beginClaim) {
|
|
36
|
-
throw new AbloValidationError(`useClaim: no \`beginClaim\` wired into SyncProvider. Pass ` +
|
|
37
|
-
`a \`beginClaim\` prop (typically bound to your transport) ` +
|
|
38
|
-
`to enable claim invocations.`, { code: 'claim_not_wired' });
|
|
39
|
-
}
|
|
40
|
-
return beginClaim(claimName, claim);
|
|
41
|
-
}, [beginClaim, claimName]);
|
|
42
|
-
}
|