@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a35d935: Fix stream-recorded undo capturing the wrong "before" value for updates. A second
|
|
8
|
+
update to the same field before the first sync-ack re-captured the original
|
|
9
|
+
pre-session value (first-old-wins + clear-only-on-ack), so undo of a quick second
|
|
10
|
+
edit jumped all the way back instead of one step. The queue now re-baselines a
|
|
11
|
+
field's tracked `.old` once its before-image is frozen into the committed
|
|
12
|
+
transaction.
|
|
13
|
+
|
|
14
|
+
Also close the create/update undo asymmetry: an update whose written key had no
|
|
15
|
+
in-place mutation produced an empty `previousData`, which made the inverse
|
|
16
|
+
un-revertible (a create's `delete` inverse never is). Before-image capture now
|
|
17
|
+
falls back to the last loaded/acked snapshot.
|
|
18
|
+
|
|
19
|
+
Internally, the two undo paths (stream-recorded and manual `RecordingTransaction`)
|
|
20
|
+
now share one before-image implementation via `Model.capturePreviousValues` /
|
|
21
|
+
`Model.consumeModifiedFields`, so they can no longer drift.
|
|
22
|
+
|
|
23
|
+
- One-correct-way consolidation (breaking; no external consumers yet, so released as a patch):
|
|
24
|
+
- Credentials collapse to a single `apiKey` — a string, or a `() => Promise<string | null>` that
|
|
25
|
+
fetches a per-user token. Removed `getToken` / `authEndpoint` / public `authToken`.
|
|
26
|
+
- `ablo.<model>.watch(ids, { ttl })` replaces the top-level `ablo.participants.join({ scope })` —
|
|
27
|
+
model-scoped read-interest + presence (WebSocket only).
|
|
28
|
+
- Read claim-gating is `ifClaimed: 'return' | 'fail'` (removed `'wait'`); waiting is the claim
|
|
29
|
+
primitive's job (`ablo.<model>.claim`).
|
|
30
|
+
- The stateless client is `Ablo({ transport: 'http' })`; `createAbloHttpClient` is no longer a
|
|
31
|
+
public export (the factory uses it internally).
|
|
32
|
+
- Read-option types renamed: `ServerReadOptions` (server `retrieve`/`list`) and `LocalReadOptions`
|
|
33
|
+
(local `get`/`getAll`).
|
|
34
|
+
- `defineSchema` throws a clear error on a reserved-field collision; the MCP/docs API surface is
|
|
35
|
+
now compile-time bound to the real exported types (can't drift).
|
|
36
|
+
|
|
37
|
+
## 0.11.1
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- 7f91f6e: DX hardening from a real onboarding session — onboarding, CLI, coordination, types, and docs.
|
|
42
|
+
|
|
43
|
+
**Client behavior**
|
|
44
|
+
- `databaseUrl` is now an explicit, server-only option: `Ablo(...)` no longer auto-reads `process.env.DATABASE_URL`. A stray `DATABASE_URL` (common — Prisma/Drizzle/docker set it) no longer silently flips the client into connection-string mode; a one-time warning points at the explicit option. Passing `databaseUrl: process.env.DATABASE_URL` explicitly is unchanged.
|
|
45
|
+
- Claims/presence are now observable from any client (including Node agents): reading a row enters its entity sync group (read-interest) and claiming pins it (write-intent), so `ablo.<model>.claim.state({ id })` reports co-participants without any manual subscribe step — whether the observer arrives before the claim (live delta) or after it (subscribe-time backfill). The claim **holder** now also sees its own claim via `claim.state`. **Requires a coordinated `sync-server` deploy** (the subscribe-time claim backfill + the entity-scope subscription gate that lets an org-authority agent key narrow into a row's group live server-side); the client package change alone does not deliver cross-client agent observation.
|
|
46
|
+
|
|
47
|
+
**CLI**
|
|
48
|
+
- `ablo init` detects the `src/app` layout (routes + the `@/ablo` import alias resolve correctly), writes the **real** stored sandbox key into `.env.local` instead of a placeholder, and scaffolds `ablo/register.ts` (a regular module, not a colliding `ablo.d.ts`).
|
|
49
|
+
- `ablo <command> --help` / `-h` now prints usage instead of erroring with "unknown flag", and `migrate` is listed in the top-level help.
|
|
50
|
+
- `ablo dev --no-watch` now exits after one push instead of watching forever.
|
|
51
|
+
|
|
52
|
+
**Types**
|
|
53
|
+
- Name the client with `typeof sync` (the value-inferred idiom, like tRPC's `typeof appRouter` / Drizzle's `typeof db`) — `ReturnType<typeof Ablo>` collapses to the untyped client and should not be used. No bespoke client-type generic is needed.
|
|
54
|
+
- `model_claim_not_configured` message clarified: claiming needs no per-model schema configuration; every model is claimable through the standard client.
|
|
55
|
+
|
|
56
|
+
**Docs**
|
|
57
|
+
- Reconciled the self-contradictory `databaseUrl` story (it is an explicit, server-only option, not auto-read from the environment; consistent casing), documented that the sandbox can host rows (apiKey only, no database), explained why a localhost Postgres can't be the system of record, and led the connect-your-database flow with `ablo pull`/`ablo check` over `ablo migrate`. Fixed stale `api.md` vocabulary (`object: 'claim'`, `participantKind: 'user' | 'agent' | 'system'`).
|
|
58
|
+
|
|
59
|
+
- 7f91f6e: Docs: document the completed `intent` → `claim` rename. Adds a 0.11.0 migration entry (`useIntent` → `useClaim`, `Register.Intents` → `Register.Claims`, `Ablo.Intent.*` → `Ablo.Claim.*`, and the coordinated client/server deploy for the `claim_*` wire frames), a `useClaim` section in the React reference, and fixes the stale `participantKind` union to the canonical `'user' | 'agent' | 'system'`.
|
|
60
|
+
|
|
3
61
|
## 0.11.0
|
|
4
62
|
|
|
5
63
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -54,10 +54,12 @@ claims are visible while the work is still in progress.
|
|
|
54
54
|
`llms.txt` · **upgrading?** see the
|
|
55
55
|
[Version History & Migration Guide](./docs/migration.md)
|
|
56
56
|
|
|
57
|
-
It works with the auth and database you already have. **
|
|
58
|
-
system of record
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
It works with the auth and database you already have. **In production, your
|
|
58
|
+
database is the system of record.** Ablo is the transaction layer on top of it:
|
|
59
|
+
realtime data is scoped to *sync groups* from your own identity, and every
|
|
60
|
+
committed row lives in your Postgres. (Trying Ablo with no database yet? The
|
|
61
|
+
hosted **sandbox** can host rows in Ablo's test plane — apiKey only, like
|
|
62
|
+
Stripe test mode — so you can explore before pointing it at your Postgres.)
|
|
61
63
|
|
|
62
64
|
**Built for** collaborative editors, AI agent workflows, and internal tools —
|
|
63
65
|
anywhere people and agents change shared state and everyone has to see it live.
|
|
@@ -65,18 +67,24 @@ anywhere people and agents change shared state and everyone has to see it live.
|
|
|
65
67
|
## Set up
|
|
66
68
|
|
|
67
69
|
The CLI takes you from nothing to a synced schema — it handles the account,
|
|
68
|
-
the key, and the env file. You bring one thing: a Postgres
|
|
69
|
-
(local, Neon, RDS — any will do
|
|
70
|
-
Ablo
|
|
70
|
+
the key, and the env file. You bring one thing: a Postgres you already have —
|
|
71
|
+
the same `DATABASE_URL` (local, Neon, RDS — any will do) that backs your auth,
|
|
72
|
+
audit, and log tables. Ablo syncs a *subset* of models against it; **in
|
|
73
|
+
production, your database is the system of record**.
|
|
71
74
|
|
|
72
75
|
```bash
|
|
73
76
|
npm install @abloatai/ablo
|
|
74
77
|
npx ablo login # opens the browser: sign in (or sign up) → a sk_test_ key is saved locally
|
|
75
78
|
npx ablo init # scaffolds ablo/schema.ts (offers to log in if you skipped it)
|
|
76
|
-
npx ablo
|
|
77
|
-
npx ablo push # pushes your schema (sandbox), writes ABLO_API_KEY to .env.local, watches for changes
|
|
79
|
+
npx ablo push # pushes your schema (sandbox), writes ABLO_API_KEY to .env.local, watches for changes
|
|
78
80
|
```
|
|
79
81
|
|
|
82
|
+
Then point Ablo at the tables for your synced models. Most teams **already
|
|
83
|
+
have those tables** (often Prisma- or Drizzle-managed) — adopt them with
|
|
84
|
+
`npx ablo pull` / `npx ablo check`, the common case. Let Ablo own its own
|
|
85
|
+
tables instead? `npx ablo migrate` provisions them in your Postgres (reads
|
|
86
|
+
`DATABASE_URL`). Either way your other tables are left untouched.
|
|
87
|
+
|
|
80
88
|
After `ablo push`, the [Quick Start](#quick-start) below runs as-is —
|
|
81
89
|
`ABLO_API_KEY` is already in `.env.local` (frameworks load it automatically;
|
|
82
90
|
plain Node: `node --env-file=.env.local app.ts`). `npx ablo status` shows
|
|
@@ -106,18 +114,24 @@ import Ablo from '@abloatai/ablo';
|
|
|
106
114
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
107
115
|
```
|
|
108
116
|
|
|
109
|
-
|
|
110
|
-
is one parameter away — no `typeof schema` re-stating, anywhere:
|
|
117
|
+
The schema is registered once (init scaffolds `ablo/register.ts` for you), and
|
|
118
|
+
every type is one parameter away — no `typeof schema` re-stating, anywhere:
|
|
111
119
|
|
|
112
120
|
```ts
|
|
113
|
-
// ablo.
|
|
114
|
-
import type { schema } from './
|
|
121
|
+
// ablo/register.ts — scaffolded by `npx ablo init`, sits beside ablo/schema.ts
|
|
122
|
+
import type { schema } from './schema';
|
|
115
123
|
declare module '@abloatai/ablo' {
|
|
116
124
|
interface Register { Schema: typeof schema }
|
|
117
125
|
}
|
|
118
126
|
export {};
|
|
119
127
|
```
|
|
120
128
|
|
|
129
|
+
It's a regular `.ts` module, not a hand-authored `.d.ts`. The top-level
|
|
130
|
+
`import type { schema }` makes the `declare module` block *merge* into (augment)
|
|
131
|
+
the SDK's `Register` interface instead of colliding with it — the same shape
|
|
132
|
+
[TanStack Router uses in `src/router.tsx`](https://tanstack.com/router/latest/docs/framework/react/guide/type-safety). Any `.ts` file in your
|
|
133
|
+
`tsconfig` `include` works; it never needs to be imported.
|
|
134
|
+
|
|
121
135
|
```ts
|
|
122
136
|
import type { Model } from '@abloatai/ablo/schema';
|
|
123
137
|
|
|
@@ -128,8 +142,29 @@ type WeatherReport = Model<'weatherReports'>; // fully typed from YOUR schema
|
|
|
128
142
|
TanStack-Router pattern: declare the source of truth once, everything
|
|
129
143
|
infers from it.)
|
|
130
144
|
|
|
145
|
+
### Naming the client type
|
|
146
|
+
|
|
147
|
+
When you need to pass the client around (a function parameter, a context value),
|
|
148
|
+
**infer the type from the value** — `type Sync = typeof sync`:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
export const sync = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
152
|
+
export type Sync = typeof sync; // fully-typed, schema-aware
|
|
153
|
+
|
|
154
|
+
function persist(client: Sync) { /* ... */ }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
This is the same idiom as tRPC's `type AppRouter = typeof appRouter` and
|
|
158
|
+
Drizzle's `typeof db` — the factory resolves the typed overload at the call
|
|
159
|
+
site, so `typeof sync` carries your schema. Do **not** write
|
|
160
|
+
`ReturnType<typeof Ablo>`: that collapses to the untyped last overload and
|
|
161
|
+
loses your model types. There is no bespoke client-type generic to import —
|
|
162
|
+
`typeof` your client value is the type.
|
|
163
|
+
|
|
131
164
|
```ts
|
|
132
165
|
const schema = defineSchema({
|
|
166
|
+
// Reserved fields (id, createdAt, updatedAt, organizationId, createdBy) are
|
|
167
|
+
// provided automatically — don't declare them.
|
|
133
168
|
weatherReports: model({
|
|
134
169
|
location: z.string(),
|
|
135
170
|
status: z.enum(['pending', 'ready']),
|
|
@@ -140,7 +175,7 @@ const schema = defineSchema({
|
|
|
140
175
|
const ablo = Ablo({
|
|
141
176
|
schema,
|
|
142
177
|
apiKey: process.env.ABLO_API_KEY, // written to .env.local by `npx ablo push`
|
|
143
|
-
databaseUrl: process.env.DATABASE_URL, // your Postgres — rows live here
|
|
178
|
+
databaseUrl: process.env.DATABASE_URL, // your Postgres, passed explicitly — rows live here
|
|
144
179
|
});
|
|
145
180
|
|
|
146
181
|
await ablo.ready();
|
|
@@ -285,7 +320,10 @@ import { AbloProvider, useAblo } from '@abloatai/ablo/react';
|
|
|
285
320
|
import { schema } from './ablo/schema';
|
|
286
321
|
|
|
287
322
|
// Build the client once — it authenticates via your session route, no key in the browser.
|
|
288
|
-
const ablo = Ablo({
|
|
323
|
+
const ablo = Ablo({
|
|
324
|
+
schema,
|
|
325
|
+
apiKey: () => fetch('/api/ablo-session').then((r) => r.text()),
|
|
326
|
+
});
|
|
289
327
|
|
|
290
328
|
function App() {
|
|
291
329
|
return (
|
|
@@ -338,7 +376,10 @@ to sync-group strings.
|
|
|
338
376
|
|
|
339
377
|
```tsx
|
|
340
378
|
// team membership is asserted server-side when the session route mints the token.
|
|
341
|
-
const ablo = Ablo({
|
|
379
|
+
const ablo = Ablo({
|
|
380
|
+
schema,
|
|
381
|
+
apiKey: () => fetch('/api/ablo-session').then((r) => r.text()),
|
|
382
|
+
});
|
|
342
383
|
|
|
343
384
|
<AbloProvider client={ablo} userId={user.id}>
|
|
344
385
|
<App />
|
|
@@ -388,29 +429,35 @@ curl https://api.abloatai.com/v1/commits \
|
|
|
388
429
|
|
|
389
430
|
## Your Database
|
|
390
431
|
|
|
391
|
-
|
|
392
|
-
layer on top of it
|
|
432
|
+
In production, every schema model is backed by **your own database** — Ablo is
|
|
433
|
+
the transaction layer on top of it. Two ways to connect it:
|
|
393
434
|
|
|
394
435
|
| | How Ablo reaches your Postgres | Use when |
|
|
395
436
|
| --- | --- | --- |
|
|
396
|
-
| **Connection string** (
|
|
437
|
+
| **Connection string** (primary) | `databaseUrl` at init — passed explicitly, never auto-read from the environment. Ablo registers the connection once (sent over TLS, stored sealed, never echoed back) and commits each write directly — through a non-superuser role, behind row-level security. | You can hand over a scoped connection string. |
|
|
397
438
|
| **Signed endpoint** | Your app exposes one route built from an ORM adapter (`prismaDataSource` / `drizzleDataSource`); Ablo sends signed commit requests and your app writes its own database. | Database credentials must never leave your infrastructure. |
|
|
398
439
|
|
|
399
|
-
|
|
440
|
+
(No database yet? The hosted **sandbox** can host rows in Ablo's test plane —
|
|
441
|
+
omit `databaseUrl` and pass an `apiKey` only, like Stripe test mode — so you can
|
|
442
|
+
try Ablo before connecting your Postgres.)
|
|
443
|
+
|
|
444
|
+
Same product, same truth either way: in production your database is the system of
|
|
445
|
+
record. See
|
|
400
446
|
[Connect Your Database](./docs/data-sources.md) for both shapes.
|
|
401
447
|
|
|
402
448
|
## Configuration
|
|
403
449
|
|
|
404
|
-
`Ablo({ ... })` takes
|
|
405
|
-
|
|
406
|
-
[Data Source endpoint](./docs/data-sources.md) in your app.
|
|
407
|
-
|
|
450
|
+
`Ablo({ ... })` takes your schema, your key, and — in production — your database,
|
|
451
|
+
either as an explicit `databaseUrl` here or as a signed
|
|
452
|
+
[Data Source endpoint](./docs/data-sources.md) in your app. (`databaseUrl` is
|
|
453
|
+
never auto-read from the environment; omit it to try Ablo against the hosted
|
|
454
|
+
sandbox.) Every other option has correct defaults:
|
|
408
455
|
|
|
409
456
|
| Option | Type | Default | Purpose |
|
|
410
457
|
| --- | --- | --- | --- |
|
|
411
458
|
| `schema` | `Schema` | — (required) | Typed model proxies (`ablo.<model>.*`) |
|
|
412
459
|
| `apiKey` | `string \| ApiKeySetter \| null` | `process.env.ABLO_API_KEY` | Server key — a string, or an async function for rotation |
|
|
413
|
-
| `databaseUrl` | `string \| null` |
|
|
460
|
+
| `databaseUrl` | `string \| null` | `—` | Your Postgres, registered as the data plane. **Must be passed explicitly — it is not auto-read from the environment.** If you have a `DATABASE_URL` set for another tool (Prisma, Drizzle, docker-compose), `Ablo()` ignores it unless you pass `databaseUrl` explicitly. Server runtimes only — the SDK throws if it sees this in a browser. Omit it when your app exposes a signed [Data Source endpoint](./docs/data-sources.md) instead, or when trying Ablo against the hosted sandbox. |
|
|
414
461
|
|
|
415
462
|
Keep `apiKey` in trusted server runtimes. In the browser, `<AbloProvider>`
|
|
416
463
|
authenticates with the signed-in user's session; the raw-key path is gated
|
package/dist/Model.d.ts
CHANGED
|
@@ -183,6 +183,45 @@ export declare abstract class Model {
|
|
|
183
183
|
* Clear tracked changes
|
|
184
184
|
*/
|
|
185
185
|
clearChanges(): void;
|
|
186
|
+
/**
|
|
187
|
+
* Capture a before-image for `keys` — the SINGLE source of truth for the
|
|
188
|
+
* "previous value" that undo inverses are built from. Both undo paths call
|
|
189
|
+
* this so they can never drift: the stream path
|
|
190
|
+
* (`TransactionQueue.extractPreviousData`) and the manual-record path
|
|
191
|
+
* (`RecordingTransaction.snapshotFields`).
|
|
192
|
+
*
|
|
193
|
+
* Resolution order per key:
|
|
194
|
+
* 1. `modifiedProperties.get(key).old` — first-old-wins pre-session
|
|
195
|
+
* baseline, set whenever the field was mutated in place before commit.
|
|
196
|
+
* 2. `getOriginalSnapshot()[key]` — the last loaded/acked row, the correct
|
|
197
|
+
* before-image for a key written WITHOUT a prior in-place mutation
|
|
198
|
+
* (e.g. a `precomputedChanges` write).
|
|
199
|
+
* 3. `fallbackToLive` only — the current live value. The manual-record path
|
|
200
|
+
* wants this last resort; the stream path deliberately OMITS unresolved
|
|
201
|
+
* keys so `buildUndoOps` drops an un-revertible inverse rather than
|
|
202
|
+
* inventing one. The flag is the one intentional difference between the
|
|
203
|
+
* two callers — do not collapse it.
|
|
204
|
+
*
|
|
205
|
+
* `id` is always skipped. Values are read out per-key, so the
|
|
206
|
+
* `getOriginalSnapshot()` "callers must not mutate" contract is preserved.
|
|
207
|
+
*
|
|
208
|
+
* Invariant this relies on: a given undo scope is EITHER stream-recorded
|
|
209
|
+
* (`recordFromStream: true`) OR manual (`useMutators({ undoScope })`), never
|
|
210
|
+
* both — otherwise a write would be captured twice. No surface sets both.
|
|
211
|
+
*/
|
|
212
|
+
capturePreviousValues(keys: Iterable<string>, opts?: {
|
|
213
|
+
fallbackToLive?: boolean;
|
|
214
|
+
}): ModelData;
|
|
215
|
+
/**
|
|
216
|
+
* Drop the `modifiedProperties` entries for `keys` — re-baselines a field
|
|
217
|
+
* after its `.old` has been frozen into a committed transaction, so the NEXT
|
|
218
|
+
* write to the same field starts from this commit's result rather than the
|
|
219
|
+
* stale pre-session `.old` that {@link propertyChanged}'s first-old-wins
|
|
220
|
+
* policy preserves. Safe because the committed transaction owns its own
|
|
221
|
+
* frozen `data`/`previousData`; neither re-reads `modifiedProperties`. `id`
|
|
222
|
+
* is never consumed. With no `keys`, consumes every tracked field.
|
|
223
|
+
*/
|
|
224
|
+
consumeModifiedFields(keys?: Iterable<string>): void;
|
|
186
225
|
/**
|
|
187
226
|
* Validate model
|
|
188
227
|
*/
|
package/dist/Model.js
CHANGED
|
@@ -223,6 +223,74 @@ export class Model {
|
|
|
223
223
|
this._originalData = this.captureSnapshot();
|
|
224
224
|
});
|
|
225
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Capture a before-image for `keys` — the SINGLE source of truth for the
|
|
228
|
+
* "previous value" that undo inverses are built from. Both undo paths call
|
|
229
|
+
* this so they can never drift: the stream path
|
|
230
|
+
* (`TransactionQueue.extractPreviousData`) and the manual-record path
|
|
231
|
+
* (`RecordingTransaction.snapshotFields`).
|
|
232
|
+
*
|
|
233
|
+
* Resolution order per key:
|
|
234
|
+
* 1. `modifiedProperties.get(key).old` — first-old-wins pre-session
|
|
235
|
+
* baseline, set whenever the field was mutated in place before commit.
|
|
236
|
+
* 2. `getOriginalSnapshot()[key]` — the last loaded/acked row, the correct
|
|
237
|
+
* before-image for a key written WITHOUT a prior in-place mutation
|
|
238
|
+
* (e.g. a `precomputedChanges` write).
|
|
239
|
+
* 3. `fallbackToLive` only — the current live value. The manual-record path
|
|
240
|
+
* wants this last resort; the stream path deliberately OMITS unresolved
|
|
241
|
+
* keys so `buildUndoOps` drops an un-revertible inverse rather than
|
|
242
|
+
* inventing one. The flag is the one intentional difference between the
|
|
243
|
+
* two callers — do not collapse it.
|
|
244
|
+
*
|
|
245
|
+
* `id` is always skipped. Values are read out per-key, so the
|
|
246
|
+
* `getOriginalSnapshot()` "callers must not mutate" contract is preserved.
|
|
247
|
+
*
|
|
248
|
+
* Invariant this relies on: a given undo scope is EITHER stream-recorded
|
|
249
|
+
* (`recordFromStream: true`) OR manual (`useMutators({ undoScope })`), never
|
|
250
|
+
* both — otherwise a write would be captured twice. No surface sets both.
|
|
251
|
+
*/
|
|
252
|
+
capturePreviousValues(keys, opts) {
|
|
253
|
+
const out = {};
|
|
254
|
+
const modified = this.modifiedProperties instanceof Map ? this.modifiedProperties : null;
|
|
255
|
+
const original = this.getOriginalSnapshot();
|
|
256
|
+
for (const key of keys) {
|
|
257
|
+
if (key === 'id')
|
|
258
|
+
continue;
|
|
259
|
+
const mod = modified?.get(key);
|
|
260
|
+
if (mod) {
|
|
261
|
+
out[key] = mod.old;
|
|
262
|
+
}
|
|
263
|
+
else if (original && key in original) {
|
|
264
|
+
out[key] = original[key];
|
|
265
|
+
}
|
|
266
|
+
else if (opts?.fallbackToLive) {
|
|
267
|
+
out[key] = Reflect.get(this, key);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Drop the `modifiedProperties` entries for `keys` — re-baselines a field
|
|
274
|
+
* after its `.old` has been frozen into a committed transaction, so the NEXT
|
|
275
|
+
* write to the same field starts from this commit's result rather than the
|
|
276
|
+
* stale pre-session `.old` that {@link propertyChanged}'s first-old-wins
|
|
277
|
+
* policy preserves. Safe because the committed transaction owns its own
|
|
278
|
+
* frozen `data`/`previousData`; neither re-reads `modifiedProperties`. `id`
|
|
279
|
+
* is never consumed. With no `keys`, consumes every tracked field.
|
|
280
|
+
*/
|
|
281
|
+
consumeModifiedFields(keys) {
|
|
282
|
+
if (!(this.modifiedProperties instanceof Map) || this.modifiedProperties.size === 0) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const only = keys ? new Set(keys) : null;
|
|
286
|
+
for (const key of [...this.modifiedProperties.keys()]) {
|
|
287
|
+
if (key === 'id')
|
|
288
|
+
continue;
|
|
289
|
+
if (only && !only.has(key))
|
|
290
|
+
continue;
|
|
291
|
+
this.modifiedProperties.delete(key);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
226
294
|
/**
|
|
227
295
|
* Validate model
|
|
228
296
|
*/
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential POLICY — the single source of truth for "what KIND of credential
|
|
3
|
+
* did the caller hand us, and what do we DO with it at connect time".
|
|
4
|
+
*
|
|
5
|
+
* Before this module the prefix-dispatch decision (`sk_`/`ek_`/`rk_`/`pk_`) was
|
|
6
|
+
* re-implemented with raw `startsWith()` sniffs in ~5 places (identity.ts ×3,
|
|
7
|
+
* auth.ts browser guard, cli/dev.ts, cli/push.ts) and the connect-time routing
|
|
8
|
+
* lived as a 4-branch if/elif tree inside `resolveParticipantIdentity`. Folding
|
|
9
|
+
* the policy here keeps the kind-taxonomy and the connect decision in ONE place;
|
|
10
|
+
* the consumers below just call into it.
|
|
11
|
+
*
|
|
12
|
+
* This module is deliberately POLICY-ONLY. It does NOT own the auth primitives
|
|
13
|
+
* (`exchangeApiKey` / `mintUserSessionKey` / `resolveIdentity`), the credential
|
|
14
|
+
* lifecycle (`startCredentialLifecycle` / refresh scheduler), or the connection
|
|
15
|
+
* FSM — those are correctly distributed consumers. `resolveCredential` DELEGATES
|
|
16
|
+
* to injected primitives rather than reimplementing any HTTP mint call.
|
|
17
|
+
*
|
|
18
|
+
* Browser-safe: `classifyCredentialKind` is a pure-string helper and MUST NOT
|
|
19
|
+
* import the Node-only `keys` module (`node:crypto`). The key-prefix contract it
|
|
20
|
+
* encodes mirrors `keys/index.ts`'s `KIND_BY_PREFIX` (the Stripe-style model:
|
|
21
|
+
* sk_=secret, rk_=restricted, ek_=ephemeral, pk_=publishable) but stays a plain
|
|
22
|
+
* prefix lookup so it can ship in the client bundle.
|
|
23
|
+
*/
|
|
24
|
+
import type { exchangeApiKey, mintUserSessionKey, resolveIdentity } from './index.js';
|
|
25
|
+
import type { resolveApiKeyValue } from '../client/auth.js';
|
|
26
|
+
/**
|
|
27
|
+
* The four Ablo API-key kinds (Stripe-style). Prefix contract — kept in lockstep
|
|
28
|
+
* with `keys/index.ts` `API_KEY_KINDS` / `KIND_BY_PREFIX`, but declared locally
|
|
29
|
+
* so this browser-safe module never pulls in `node:crypto`.
|
|
30
|
+
*/
|
|
31
|
+
export type CredentialKind = 'secret' | 'ephemeral' | 'restricted' | 'publishable';
|
|
32
|
+
/**
|
|
33
|
+
* Lightweight, browser-safe prefix → kind classifier. The SINGLE source of truth
|
|
34
|
+
* for prefix dispatch across the SDK (connect routing, the browser guard, the
|
|
35
|
+
* CLI key-gating). Returns `null` for a value that carries no recognized Ablo
|
|
36
|
+
* key prefix (a caller-supplied capability/auth token, an empty/garbage value).
|
|
37
|
+
*
|
|
38
|
+
* Pure string check — does NOT validate the checksum or environment segment
|
|
39
|
+
* (that's `keys/index.ts` `parseApiKey`, which is Node-only). This is only the
|
|
40
|
+
* "which of the four buckets" decision.
|
|
41
|
+
*/
|
|
42
|
+
export declare function classifyCredentialKind(value: string): CredentialKind | null;
|
|
43
|
+
/**
|
|
44
|
+
* Auth primitives injected into {@link resolveCredential}. Each is the canonical
|
|
45
|
+
* implementation from `auth/index.ts` / `client/auth.ts`; the policy DELEGATES to
|
|
46
|
+
* them so the HTTP mint logic stays in ONE place and only the routing decision
|
|
47
|
+
* lives here. `mintUserSessionKey` is carried for completeness of the primitive
|
|
48
|
+
* surface (the browser/session path mints it before connect); `resolveCredential`
|
|
49
|
+
* never re-mints it — a pre-minted `ek_` arrives ready to use.
|
|
50
|
+
*/
|
|
51
|
+
export interface CredentialPrimitives {
|
|
52
|
+
readonly exchangeApiKey: typeof exchangeApiKey;
|
|
53
|
+
readonly mintUserSessionKey: typeof mintUserSessionKey;
|
|
54
|
+
readonly resolveIdentity: typeof resolveIdentity;
|
|
55
|
+
readonly resolveApiKeyValue: typeof resolveApiKeyValue;
|
|
56
|
+
}
|
|
57
|
+
export interface ResolveCredentialContext {
|
|
58
|
+
readonly primitives: CredentialPrimitives;
|
|
59
|
+
/**
|
|
60
|
+
* Build the argument bag for the hosted exchange. identity.ts owns the baseUrl
|
|
61
|
+
* derivation + participant scope, so it supplies the args; the policy invokes
|
|
62
|
+
* `primitives.exchangeApiKey` with them. The `apiKey` is filled in by the policy
|
|
63
|
+
* from the resolved value.
|
|
64
|
+
*/
|
|
65
|
+
readonly exchangeArgs: Omit<Parameters<typeof exchangeApiKey>[0], 'apiKey'>;
|
|
66
|
+
}
|
|
67
|
+
export interface ResolveCredentialInput {
|
|
68
|
+
/** Resolved string value of the configured `apiKey` (callable already invoked), or null. */
|
|
69
|
+
readonly apiKeyValue: string | null;
|
|
70
|
+
/** The configured `apiKey` (string or setter) — threaded onto the refresh path. */
|
|
71
|
+
readonly configuredApiKey: string | (() => Promise<string | null>) | null;
|
|
72
|
+
/** Explicit caller-supplied capability token (`options.capabilityToken`). */
|
|
73
|
+
readonly capabilityToken: string | undefined;
|
|
74
|
+
/** Configured static `authToken`. */
|
|
75
|
+
readonly authToken: string | null;
|
|
76
|
+
/** True once the caller knows its own identity (legacy explicit path). */
|
|
77
|
+
readonly hasExplicitIdentity: boolean;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The connect-time decision, expressed as a discriminated union over the routing
|
|
81
|
+
* kind (NOT the raw key kind — `ek_` and `rk_` collapse into the same
|
|
82
|
+
* `pre-minted` route, and a bare capability token routes the same way). The
|
|
83
|
+
* caller (`identity.ts`) switches on `kind` and performs the scope/side-effect
|
|
84
|
+
* wiring each route needs.
|
|
85
|
+
*
|
|
86
|
+
* Fields carry exactly what each branch in the old if/elif tree produced:
|
|
87
|
+
* - `getBearer` — the token to authenticate the bootstrap/`/auth/*` HTTP
|
|
88
|
+
* and to seed the credential source with.
|
|
89
|
+
* - `expiresAtMs` — exchange expiry (drives the refresh scheduler) or null
|
|
90
|
+
* when the credential never expires / nothing to refresh.
|
|
91
|
+
* - `controlPlaneKey` — the ORIGINAL configured apiKey when the route minted
|
|
92
|
+
* via exchange (so a refresh can re-mint), else null.
|
|
93
|
+
*/
|
|
94
|
+
export type ResolvedCredential =
|
|
95
|
+
/** `pk_` — long-lived browser-safe read-only project key. Used directly as the
|
|
96
|
+
* bearer; never exchanged, never refreshed. Identity resolved via `/auth/identity`. */
|
|
97
|
+
{
|
|
98
|
+
readonly kind: 'publishable';
|
|
99
|
+
readonly getBearer: string;
|
|
100
|
+
readonly expiresAtMs: null;
|
|
101
|
+
readonly controlPlaneKey: null;
|
|
102
|
+
}
|
|
103
|
+
/** `sk_` (no explicit cap token) — hosted-cloud. Exchanged for a capability
|
|
104
|
+
* token via `exchangeApiKey`; the refresh scheduler re-mints before expiry. */
|
|
105
|
+
| {
|
|
106
|
+
readonly kind: 'exchange';
|
|
107
|
+
/** Result of the initial `exchangeApiKey` call. */
|
|
108
|
+
readonly exchange: Awaited<ReturnType<typeof exchangeApiKey>>;
|
|
109
|
+
readonly getBearer: string;
|
|
110
|
+
readonly expiresAtMs: number;
|
|
111
|
+
/** The configured apiKey (string or setter) — read fresh on each refresh. */
|
|
112
|
+
readonly controlPlaneKey: string | (() => Promise<string | null>);
|
|
113
|
+
}
|
|
114
|
+
/** Pre-minted `ek_`/`rk_` OR an explicit capability/auth token — used AS-IS as
|
|
115
|
+
* the bearer (never exchanged). Identity resolved via `/auth/identity`. */
|
|
116
|
+
| {
|
|
117
|
+
readonly kind: 'pre-minted';
|
|
118
|
+
readonly getBearer: string;
|
|
119
|
+
readonly expiresAtMs: null;
|
|
120
|
+
readonly controlPlaneKey: null;
|
|
121
|
+
}
|
|
122
|
+
/** Legacy explicit — caller knows its own organizationId + user/agentId. No
|
|
123
|
+
* server round-trip; the (optional) bearer is the initial cap token. */
|
|
124
|
+
| {
|
|
125
|
+
readonly kind: 'explicit';
|
|
126
|
+
readonly getBearer: string | undefined;
|
|
127
|
+
readonly expiresAtMs: null;
|
|
128
|
+
readonly controlPlaneKey: null;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Connect-time credential routing — absorbs the decision tree that used to live
|
|
132
|
+
* inline in `resolveParticipantIdentity`. Classifies the configured apiKey, then
|
|
133
|
+
* routes to one of four outcomes, DELEGATING the actual HTTP exchange to the
|
|
134
|
+
* injected `exchangeApiKey` primitive. The caller switches on
|
|
135
|
+
* `ResolvedCredential.kind` to perform scope wiring + scheduler setup.
|
|
136
|
+
*
|
|
137
|
+
* Routing (preserves the old branch order exactly):
|
|
138
|
+
* 0. `pk_` + no explicit cap token → `publishable` (direct bearer, no refresh).
|
|
139
|
+
* 1. exchangeable apiKey (any prefix that ISN'T a pre-minted `ek_`/`rk_`) +
|
|
140
|
+
* no explicit cap token → `exchange` (hosted-cloud round-trip + scheduler).
|
|
141
|
+
* 2. otherwise, identity unknown → `pre-minted` (use the cap token as-is). Throws
|
|
142
|
+
* `session_expired` when there is no token to authenticate `/auth/identity`.
|
|
143
|
+
* 3. otherwise (identity known) → `explicit` (legacy self-hosted, no round-trip).
|
|
144
|
+
*/
|
|
145
|
+
export declare function resolveCredential(input: ResolveCredentialInput, ctx: ResolveCredentialContext): Promise<ResolvedCredential>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential POLICY — the single source of truth for "what KIND of credential
|
|
3
|
+
* did the caller hand us, and what do we DO with it at connect time".
|
|
4
|
+
*
|
|
5
|
+
* Before this module the prefix-dispatch decision (`sk_`/`ek_`/`rk_`/`pk_`) was
|
|
6
|
+
* re-implemented with raw `startsWith()` sniffs in ~5 places (identity.ts ×3,
|
|
7
|
+
* auth.ts browser guard, cli/dev.ts, cli/push.ts) and the connect-time routing
|
|
8
|
+
* lived as a 4-branch if/elif tree inside `resolveParticipantIdentity`. Folding
|
|
9
|
+
* the policy here keeps the kind-taxonomy and the connect decision in ONE place;
|
|
10
|
+
* the consumers below just call into it.
|
|
11
|
+
*
|
|
12
|
+
* This module is deliberately POLICY-ONLY. It does NOT own the auth primitives
|
|
13
|
+
* (`exchangeApiKey` / `mintUserSessionKey` / `resolveIdentity`), the credential
|
|
14
|
+
* lifecycle (`startCredentialLifecycle` / refresh scheduler), or the connection
|
|
15
|
+
* FSM — those are correctly distributed consumers. `resolveCredential` DELEGATES
|
|
16
|
+
* to injected primitives rather than reimplementing any HTTP mint call.
|
|
17
|
+
*
|
|
18
|
+
* Browser-safe: `classifyCredentialKind` is a pure-string helper and MUST NOT
|
|
19
|
+
* import the Node-only `keys` module (`node:crypto`). The key-prefix contract it
|
|
20
|
+
* encodes mirrors `keys/index.ts`'s `KIND_BY_PREFIX` (the Stripe-style model:
|
|
21
|
+
* sk_=secret, rk_=restricted, ek_=ephemeral, pk_=publishable) but stays a plain
|
|
22
|
+
* prefix lookup so it can ship in the client bundle.
|
|
23
|
+
*/
|
|
24
|
+
import { AbloAuthenticationError } from '../errors.js';
|
|
25
|
+
const KIND_BY_PREFIX = [
|
|
26
|
+
['sk_', 'secret'],
|
|
27
|
+
['ek_', 'ephemeral'],
|
|
28
|
+
['rk_', 'restricted'],
|
|
29
|
+
['pk_', 'publishable'],
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Lightweight, browser-safe prefix → kind classifier. The SINGLE source of truth
|
|
33
|
+
* for prefix dispatch across the SDK (connect routing, the browser guard, the
|
|
34
|
+
* CLI key-gating). Returns `null` for a value that carries no recognized Ablo
|
|
35
|
+
* key prefix (a caller-supplied capability/auth token, an empty/garbage value).
|
|
36
|
+
*
|
|
37
|
+
* Pure string check — does NOT validate the checksum or environment segment
|
|
38
|
+
* (that's `keys/index.ts` `parseApiKey`, which is Node-only). This is only the
|
|
39
|
+
* "which of the four buckets" decision.
|
|
40
|
+
*/
|
|
41
|
+
export function classifyCredentialKind(value) {
|
|
42
|
+
for (const [prefix, kind] of KIND_BY_PREFIX) {
|
|
43
|
+
if (value.startsWith(prefix))
|
|
44
|
+
return kind;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Connect-time credential routing — absorbs the decision tree that used to live
|
|
50
|
+
* inline in `resolveParticipantIdentity`. Classifies the configured apiKey, then
|
|
51
|
+
* routes to one of four outcomes, DELEGATING the actual HTTP exchange to the
|
|
52
|
+
* injected `exchangeApiKey` primitive. The caller switches on
|
|
53
|
+
* `ResolvedCredential.kind` to perform scope wiring + scheduler setup.
|
|
54
|
+
*
|
|
55
|
+
* Routing (preserves the old branch order exactly):
|
|
56
|
+
* 0. `pk_` + no explicit cap token → `publishable` (direct bearer, no refresh).
|
|
57
|
+
* 1. exchangeable apiKey (any prefix that ISN'T a pre-minted `ek_`/`rk_`) +
|
|
58
|
+
* no explicit cap token → `exchange` (hosted-cloud round-trip + scheduler).
|
|
59
|
+
* 2. otherwise, identity unknown → `pre-minted` (use the cap token as-is). Throws
|
|
60
|
+
* `session_expired` when there is no token to authenticate `/auth/identity`.
|
|
61
|
+
* 3. otherwise (identity known) → `explicit` (legacy self-hosted, no round-trip).
|
|
62
|
+
*/
|
|
63
|
+
export async function resolveCredential(input, ctx) {
|
|
64
|
+
const { apiKeyValue, capabilityToken, authToken, hasExplicitIdentity } = input;
|
|
65
|
+
const kind = apiKeyValue != null ? classifyCredentialKind(apiKeyValue) : null;
|
|
66
|
+
// A pre-minted capability bearer (`ek_` ephemeral / `rk_` restricted) is NOT
|
|
67
|
+
// exchangeable — it was already minted into the credential source before
|
|
68
|
+
// connect and must be USED DIRECTLY as the bearer (Route 2), never sent through
|
|
69
|
+
// `exchangeApiKey` (Route 1, which expects an `sk_`).
|
|
70
|
+
const isPreMintedCapabilityBearer = kind === 'ephemeral' || kind === 'restricted';
|
|
71
|
+
const initialCapToken = capabilityToken ??
|
|
72
|
+
(isPreMintedCapabilityBearer ? apiKeyValue ?? undefined : undefined) ??
|
|
73
|
+
authToken ??
|
|
74
|
+
undefined;
|
|
75
|
+
// Route 0: publishable key (`pk_`) — long-lived, browser-safe, READ-ONLY. Used
|
|
76
|
+
// DIRECTLY as the bearer; never exchanged → never expires → nothing to refresh.
|
|
77
|
+
if (apiKeyValue != null && kind === 'publishable' && capabilityToken == null) {
|
|
78
|
+
return {
|
|
79
|
+
kind: 'publishable',
|
|
80
|
+
getBearer: apiKeyValue,
|
|
81
|
+
expiresAtMs: null,
|
|
82
|
+
controlPlaneKey: null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Route 1: hosted-cloud (secret/exchangeable apiKey, no caller-supplied cap
|
|
86
|
+
// token). A pre-minted `ek_`/`rk_` is NOT exchangeable → falls through.
|
|
87
|
+
if (apiKeyValue != null &&
|
|
88
|
+
capabilityToken == null &&
|
|
89
|
+
!isPreMintedCapabilityBearer) {
|
|
90
|
+
const exchange = await ctx.primitives.exchangeApiKey({
|
|
91
|
+
...ctx.exchangeArgs,
|
|
92
|
+
apiKey: apiKeyValue,
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
kind: 'exchange',
|
|
96
|
+
exchange,
|
|
97
|
+
getBearer: exchange.token,
|
|
98
|
+
expiresAtMs: Date.parse(exchange.expiresAt),
|
|
99
|
+
controlPlaneKey: input.configuredApiKey ?? apiKeyValue,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Route 2: self-derived / pre-minted (use the cap token as-is). Reached when
|
|
103
|
+
// identity is NOT caller-supplied.
|
|
104
|
+
if (!hasExplicitIdentity) {
|
|
105
|
+
if (initialCapToken == null) {
|
|
106
|
+
// No apiKey to exchange (Route 1) and no caller-supplied identity (Route 3),
|
|
107
|
+
// so `initialCapToken` is the only thing that could authenticate
|
|
108
|
+
// `/auth/identity`. Absent — commonly the function `apiKey` resolver
|
|
109
|
+
// returning `null` (no/expired session) — surface the real, re-auth-able
|
|
110
|
+
// condition locally instead of making a doomed round-trip.
|
|
111
|
+
throw new AbloAuthenticationError('No auth token available to resolve identity — the session token is ' +
|
|
112
|
+
'missing or expired. Ensure your `apiKey` resolver returns a valid token, or ' +
|
|
113
|
+
'pass a static `apiKey` / `capabilityToken`.', { code: 'session_expired' });
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
kind: 'pre-minted',
|
|
117
|
+
getBearer: initialCapToken,
|
|
118
|
+
expiresAtMs: null,
|
|
119
|
+
controlPlaneKey: null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Route 3: legacy explicit (self-hosted — caller knows its own
|
|
123
|
+
// organizationId + user/agentId).
|
|
124
|
+
return {
|
|
125
|
+
kind: 'explicit',
|
|
126
|
+
getBearer: initialCapToken,
|
|
127
|
+
expiresAtMs: null,
|
|
128
|
+
controlPlaneKey: null,
|
|
129
|
+
};
|
|
130
|
+
}
|