@abloatai/ablo 0.9.9 → 0.9.11
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 +17 -0
- package/README.md +47 -8
- package/dist/auth/index.d.ts +25 -3
- package/dist/auth/index.js +61 -1
- package/dist/auth/schemas.d.ts +17 -0
- package/dist/auth/schemas.js +24 -0
- package/dist/cli.cjs +19 -4
- package/dist/client/Ablo.d.ts +31 -21
- package/dist/client/Ablo.js +77 -22
- package/dist/errorCodes.d.ts +1 -0
- package/dist/errorCodes.js +1 -0
- package/dist/react/AbloProvider.d.ts +0 -2
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +2 -2
- package/dist/schema/roles.d.ts +18 -0
- package/dist/schema/roles.js +11 -0
- package/dist/schema/schema.d.ts +24 -1
- package/dist/schema/schema.js +1 -1
- package/dist/server/storage-mode.d.ts +1 -24
- package/docs/api-keys.md +16 -0
- package/docs/quickstart.md +23 -0
- package/llms-full.txt +3 -0
- package/llms.txt +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- `Model<'name'>` type helper via the `Register` binding — name your model in one parameter (`Model<'tasks'>`) instead of restating `typeof schema`; `Model<S, 'name'>` is also supported and `InferModel` is deprecated. CLI: retire the stale `dev` wording from the login outro and `push` header. Docs: cover the `Register` binding end-to-end and document the `pk_` publishable key + the `/v1/commits` HTTP path.
|
|
8
|
+
- 3024593: Fix `sessions.create({ user })` 403 — user sessions now mint via the sk\_-gated ephemeral-key door
|
|
9
|
+
- `sessions.create({ user })` mints an `ek_` user session via `/auth/ephemeral-keys` (was wrongly routed through `/auth/capability`, which rejects human participants — writes were being attributed to agents).
|
|
10
|
+
- Control-plane calls always present your original `sk_`, never the client's exchanged sync credential.
|
|
11
|
+
- `sessions.create({ agent, can })` no longer requires hand-built `syncGroups` — the org anchor is the server default — and the `can` allowlist is now honored at commit time (model-alias matching).
|
|
12
|
+
- New: `ablo.organizationId` (resolved after `ready()`), `ablo status --json`, typed sync-group inputs (`SyncGroupInput` + `invalid_sync_group` rejection for malformed groups).
|
|
13
|
+
|
|
14
|
+
## 0.9.10
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- README: add a centered brand header (Ablo banner, tagline, doc nav links, and status badges).
|
|
19
|
+
|
|
3
20
|
## 0.9.9
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://abloatai.com"><img src="assets/banner.png" alt="Ablo" width="480" /></a>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<strong>Let people and AI agents work on the same data without overwriting each other.</strong>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://abloatai.com">Docs</a> |
|
|
11
|
+
<a href="https://abloatai.com/quickstart">Quickstart</a> |
|
|
12
|
+
<a href="https://abloatai.com/api">API</a> |
|
|
13
|
+
<a href="https://github.com/Abloatai/ablo">GitHub</a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://www.npmjs.com/package/@abloatai/ablo"><img src="https://img.shields.io/npm/v/@abloatai/ablo?style=flat-square&color=2563eb" alt="npm" /></a>
|
|
18
|
+
<a href="https://abloatai.com"><img src="https://img.shields.io/badge/docs-abloatai.com-2563eb?style=flat-square" alt="docs" /></a>
|
|
19
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-2563eb?style=flat-square" alt="license" /></a>
|
|
20
|
+
<img src="https://img.shields.io/badge/node-%E2%89%A524-22c55e?style=flat-square" alt="node >=24" />
|
|
21
|
+
<img src="https://img.shields.io/badge/types-included-2563eb?style=flat-square" alt="types included" />
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
---
|
|
9
25
|
|
|
10
26
|
When an agent and a person change the same thing at once, work gets lost: one
|
|
11
27
|
edit silently clobbers another, or the agent acts on data that already moved.
|
|
@@ -85,6 +101,29 @@ instead of guessing:
|
|
|
85
101
|
import Ablo from '@abloatai/ablo';
|
|
86
102
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
87
103
|
|
|
104
|
+
Register the schema once (init scaffolds this `ablo.d.ts`), and every type
|
|
105
|
+
is one parameter away — no `typeof schema` re-stating, anywhere:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// ablo.d.ts — once per project
|
|
109
|
+
import type { schema } from './ablo/schema';
|
|
110
|
+
declare module '@abloatai/ablo' {
|
|
111
|
+
interface Register { Schema: typeof schema }
|
|
112
|
+
}
|
|
113
|
+
export {};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import type { Model } from '@abloatai/ablo/schema';
|
|
118
|
+
|
|
119
|
+
type WeatherReport = Model<'weatherReports'>; // fully typed from YOUR schema
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
(The same `Register` binding types every hook and client — it's the
|
|
123
|
+
TanStack-Router pattern: declare the source of truth once, everything
|
|
124
|
+
infers from it.)
|
|
125
|
+
|
|
126
|
+
|
|
88
127
|
const schema = defineSchema({
|
|
89
128
|
weatherReports: model({
|
|
90
129
|
location: z.string(),
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* SDKs hide their internal auth-handshake — the apiKey is the only
|
|
12
12
|
* credential the consumer touches.
|
|
13
13
|
*/
|
|
14
|
-
import { type CapabilityExchangeResponse, type IdentityResolveResponse } from './schemas.js';
|
|
15
|
-
export type { CapabilityExchangeResponse, IdentityResolveResponse } from './schemas.js';
|
|
14
|
+
import { type CapabilityExchangeResponse, type EphemeralKeyResponse, type IdentityResolveResponse } from './schemas.js';
|
|
15
|
+
export type { CapabilityExchangeResponse, EphemeralKeyResponse, IdentityResolveResponse, } from './schemas.js';
|
|
16
16
|
export interface ExchangeApiKeyRequest {
|
|
17
17
|
readonly apiKey: string;
|
|
18
18
|
readonly baseUrl: string;
|
|
@@ -20,7 +20,6 @@ export interface ExchangeApiKeyRequest {
|
|
|
20
20
|
readonly participantId?: string;
|
|
21
21
|
readonly syncGroups?: readonly string[];
|
|
22
22
|
readonly operations?: readonly string[];
|
|
23
|
-
readonly wideScope?: boolean;
|
|
24
23
|
readonly ttlSeconds: number;
|
|
25
24
|
readonly label?: string;
|
|
26
25
|
readonly userMeta?: Record<string, unknown>;
|
|
@@ -28,6 +27,29 @@ export interface ExchangeApiKeyRequest {
|
|
|
28
27
|
readonly timeoutMs?: number;
|
|
29
28
|
}
|
|
30
29
|
export declare function exchangeApiKey(options: ExchangeApiKeyRequest): Promise<CapabilityExchangeResponse>;
|
|
30
|
+
export interface MintUserSessionRequest {
|
|
31
|
+
/** The ORIGINAL secret (`sk_`) key — control-plane calls always present it,
|
|
32
|
+
* never the exchanged sync credential. */
|
|
33
|
+
readonly apiKey: string;
|
|
34
|
+
readonly baseUrl: string;
|
|
35
|
+
/** The end user's external IdP id — becomes the session's `participantId`. */
|
|
36
|
+
readonly userId: string;
|
|
37
|
+
readonly syncGroups?: readonly string[];
|
|
38
|
+
readonly ttlSeconds: number;
|
|
39
|
+
readonly label?: string;
|
|
40
|
+
readonly fetch?: typeof fetch;
|
|
41
|
+
readonly timeoutMs?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Mint an END-USER session key (`ek_`) via `POST /auth/ephemeral-keys` — the
|
|
45
|
+
* sk_-gated user-session door. This is deliberately a DIFFERENT endpoint from
|
|
46
|
+
* `/auth/capability`: that route can never mint humans (its
|
|
47
|
+
* `invalid_participant_kind` gate is what fired in the 2026-06-11 Pulse
|
|
48
|
+
* cascade, when `sessions.create({ user })` was funneled through the agent
|
|
49
|
+
* door). The server trusts the `ek_` because a secret key minted it; the
|
|
50
|
+
* browser presents it as its bearer.
|
|
51
|
+
*/
|
|
52
|
+
export declare function mintUserSessionKey(options: MintUserSessionRequest): Promise<EphemeralKeyResponse>;
|
|
31
53
|
export interface ResolveIdentityRequest {
|
|
32
54
|
readonly baseUrl: string;
|
|
33
55
|
readonly authToken?: string;
|
package/dist/auth/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* SDKs hide their internal auth-handshake — the apiKey is the only
|
|
12
12
|
* credential the consumer touches.
|
|
13
13
|
*/
|
|
14
|
-
import { parseCapabilityExchangeResponse, parseIdentityResolveResponse, } from './schemas.js';
|
|
14
|
+
import { parseCapabilityExchangeResponse, parseEphemeralKeyResponse, parseIdentityResolveResponse, } from './schemas.js';
|
|
15
15
|
import { AbloAuthenticationError, hasWireCode, translateHttpError } from '../errors.js';
|
|
16
16
|
export async function exchangeApiKey(options) {
|
|
17
17
|
if (!options.apiKey) {
|
|
@@ -75,6 +75,66 @@ export async function exchangeApiKey(options) {
|
|
|
75
75
|
}
|
|
76
76
|
return parseCapabilityExchangeResponse(await response.json());
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Mint an END-USER session key (`ek_`) via `POST /auth/ephemeral-keys` — the
|
|
80
|
+
* sk_-gated user-session door. This is deliberately a DIFFERENT endpoint from
|
|
81
|
+
* `/auth/capability`: that route can never mint humans (its
|
|
82
|
+
* `invalid_participant_kind` gate is what fired in the 2026-06-11 Pulse
|
|
83
|
+
* cascade, when `sessions.create({ user })` was funneled through the agent
|
|
84
|
+
* door). The server trusts the `ek_` because a secret key minted it; the
|
|
85
|
+
* browser presents it as its bearer.
|
|
86
|
+
*/
|
|
87
|
+
export async function mintUserSessionKey(options) {
|
|
88
|
+
if (!options.apiKey) {
|
|
89
|
+
throw new AbloAuthenticationError('No API key found. Set ABLO_API_KEY in your environment or pass `apiKey` ' +
|
|
90
|
+
'to Ablo({ ... }) directly — user sessions are minted by your backend.', { code: 'apikey_missing' });
|
|
91
|
+
}
|
|
92
|
+
if (!options.baseUrl) {
|
|
93
|
+
throw new AbloAuthenticationError('baseUrl is required for user-session mint', { code: 'base_url_missing' });
|
|
94
|
+
}
|
|
95
|
+
const fetcher = options.fetch ?? fetch;
|
|
96
|
+
const url = `${options.baseUrl.replace(/\/+$/, '')}/auth/ephemeral-keys`;
|
|
97
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
100
|
+
let response;
|
|
101
|
+
try {
|
|
102
|
+
response = await fetcher(url, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: {
|
|
105
|
+
'Content-Type': 'application/json',
|
|
106
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
user: { id: options.userId },
|
|
110
|
+
...(options.syncGroups ? { syncGroups: options.syncGroups } : {}),
|
|
111
|
+
ttlSeconds: options.ttlSeconds,
|
|
112
|
+
...(options.label ? { label: options.label } : {}),
|
|
113
|
+
}),
|
|
114
|
+
signal: controller.signal,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
throw new AbloAuthenticationError(`user-session mint failed: ${err instanceof Error ? err.message : String(err)}`, { code: 'exchange_network_error', cause: err });
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
}
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
let body = null;
|
|
125
|
+
try {
|
|
126
|
+
body = await response.json();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// ignore — server returned non-JSON error
|
|
130
|
+
}
|
|
131
|
+
const requestId = response.headers.get('x-request-id') ?? undefined;
|
|
132
|
+
throw hasWireCode(body)
|
|
133
|
+
? translateHttpError(response.status, body, requestId)
|
|
134
|
+
: new AbloAuthenticationError(`user-session mint rejected (${response.status})`, { code: 'exchange_failed', httpStatus: response.status });
|
|
135
|
+
}
|
|
136
|
+
return parseEphemeralKeyResponse(await response.json());
|
|
137
|
+
}
|
|
78
138
|
/**
|
|
79
139
|
* Resolve the caller's Ablo identity from the authenticated request
|
|
80
140
|
* context. Used by browser/session/capability flows where the SDK should
|
package/dist/auth/schemas.d.ts
CHANGED
|
@@ -31,5 +31,22 @@ export declare const IdentityResolveResponseSchema: z.ZodObject<{
|
|
|
31
31
|
userMeta: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
32
32
|
}, z.core.$loose>;
|
|
33
33
|
export type IdentityResolveResponse = z.infer<typeof IdentityResolveResponseSchema>;
|
|
34
|
+
/**
|
|
35
|
+
* Response of `POST /auth/ephemeral-keys` — the sk_-gated END-USER session
|
|
36
|
+
* mint (`ek_`). Flat shape (no `scope` block): the server stores the scope on
|
|
37
|
+
* the key row and re-derives it at every verify; the client only needs the
|
|
38
|
+
* token + identity facts to hand to the browser.
|
|
39
|
+
*/
|
|
40
|
+
export declare const EphemeralKeyResponseSchema: z.ZodObject<{
|
|
41
|
+
object: z.ZodOptional<z.ZodLiteral<"ephemeral_key">>;
|
|
42
|
+
id: z.ZodString;
|
|
43
|
+
token: z.ZodString;
|
|
44
|
+
expiresAt: z.ZodString;
|
|
45
|
+
organizationId: z.ZodString;
|
|
46
|
+
participantId: z.ZodString;
|
|
47
|
+
syncGroups: z.ZodArray<z.ZodString>;
|
|
48
|
+
}, z.core.$loose>;
|
|
49
|
+
export type EphemeralKeyResponse = z.infer<typeof EphemeralKeyResponseSchema>;
|
|
34
50
|
export declare function parseCapabilityExchangeResponse(raw: unknown): CapabilityExchangeResponse;
|
|
51
|
+
export declare function parseEphemeralKeyResponse(raw: unknown): EphemeralKeyResponse;
|
|
35
52
|
export declare function parseIdentityResolveResponse(raw: unknown): IdentityResolveResponse;
|
package/dist/auth/schemas.js
CHANGED
|
@@ -29,6 +29,23 @@ export const IdentityResolveResponseSchema = z
|
|
|
29
29
|
userMeta: z.record(z.string(), z.unknown()),
|
|
30
30
|
})
|
|
31
31
|
.passthrough();
|
|
32
|
+
/**
|
|
33
|
+
* Response of `POST /auth/ephemeral-keys` — the sk_-gated END-USER session
|
|
34
|
+
* mint (`ek_`). Flat shape (no `scope` block): the server stores the scope on
|
|
35
|
+
* the key row and re-derives it at every verify; the client only needs the
|
|
36
|
+
* token + identity facts to hand to the browser.
|
|
37
|
+
*/
|
|
38
|
+
export const EphemeralKeyResponseSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
object: z.literal('ephemeral_key').optional(),
|
|
41
|
+
id: z.string().min(1),
|
|
42
|
+
token: AuthTokenSchema,
|
|
43
|
+
expiresAt: z.string().min(1),
|
|
44
|
+
organizationId: z.string().min(1),
|
|
45
|
+
participantId: z.string().min(1),
|
|
46
|
+
syncGroups: z.array(z.string()),
|
|
47
|
+
})
|
|
48
|
+
.passthrough();
|
|
32
49
|
function formatIssues(error) {
|
|
33
50
|
return error.issues
|
|
34
51
|
.map((issue) => {
|
|
@@ -44,6 +61,13 @@ export function parseCapabilityExchangeResponse(raw) {
|
|
|
44
61
|
}
|
|
45
62
|
return parsed.data;
|
|
46
63
|
}
|
|
64
|
+
export function parseEphemeralKeyResponse(raw) {
|
|
65
|
+
const parsed = EphemeralKeyResponseSchema.safeParse(raw);
|
|
66
|
+
if (!parsed.success) {
|
|
67
|
+
throw new AbloAuthenticationError(`user-session mint response was malformed: ${formatIssues(parsed.error)}`, { code: 'exchange_malformed_response', cause: parsed.error });
|
|
68
|
+
}
|
|
69
|
+
return parsed.data;
|
|
70
|
+
}
|
|
47
71
|
export function parseIdentityResolveResponse(raw) {
|
|
48
72
|
const parsed = IdentityResolveResponseSchema.safeParse(raw);
|
|
49
73
|
if (!parsed.success) {
|
package/dist/cli.cjs
CHANGED
|
@@ -276983,6 +276983,7 @@ var ERROR_CODES = {
|
|
|
276983
276983
|
invalid_request: wire("validation", 400, false, "The request parameters were invalid."),
|
|
276984
276984
|
capability_not_found: wire("not_found", 404, false, "No capability exists with the given id."),
|
|
276985
276985
|
invalid_participant_kind: wire("validation", 400, false, "The participant kind is invalid."),
|
|
276986
|
+
invalid_sync_group: wire("validation", 400, false, 'Sync groups must be "default" or "<namespace>:<id>".'),
|
|
276986
276987
|
narrow_scope_required: wire("validation", 400, false, "A narrowed scope is required for this request."),
|
|
276987
276988
|
wide_scope_forbidden: wire("permission", 403, false, "A wide scope is not permitted for this caller."),
|
|
276988
276989
|
capability_required: wire("auth", 401, false, "This operation requires a capability."),
|
|
@@ -280053,7 +280054,7 @@ async function dev(argv) {
|
|
|
280053
280054
|
process.exit(1);
|
|
280054
280055
|
}
|
|
280055
280056
|
console.log(`
|
|
280056
|
-
${brand("ablo")} ${import_picocolors6.default.dim("
|
|
280057
|
+
${brand("ablo")} ${import_picocolors6.default.dim("push")} ${import_picocolors6.default.dim("(sandbox)")}
|
|
280057
280058
|
`);
|
|
280058
280059
|
const projectDbUrl = readProjectDatabaseUrl();
|
|
280059
280060
|
if (projectDbUrl) await ensureScopedRoleInteractive(projectDbUrl);
|
|
@@ -280241,7 +280242,7 @@ ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
|
|
|
280241
280242
|
...prov.live ? { production: entry(prov.live) } : {}
|
|
280242
280243
|
});
|
|
280243
280244
|
s.stop(`Saved keys to ${path}`);
|
|
280244
|
-
Se(`${import_picocolors7.default.green("\u2713")} Logged in ${import_picocolors7.default.dim("(sandbox)")}. Run ${import_picocolors7.default.bold("ablo
|
|
280245
|
+
Se(`${import_picocolors7.default.green("\u2713")} Logged in ${import_picocolors7.default.dim("(sandbox)")}. Run ${import_picocolors7.default.bold("npx ablo push")} to push your schema.`);
|
|
280245
280246
|
}
|
|
280246
280247
|
async function login() {
|
|
280247
280248
|
await deviceLogin();
|
|
@@ -280336,10 +280337,23 @@ async function ping(apiUrl) {
|
|
|
280336
280337
|
clearTimeout(t);
|
|
280337
280338
|
}
|
|
280338
280339
|
}
|
|
280339
|
-
async function status() {
|
|
280340
|
+
async function status(args = []) {
|
|
280340
280341
|
const apiUrl = (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
|
|
280341
280342
|
const cfg = readConfig();
|
|
280342
280343
|
const mode2 = getMode();
|
|
280344
|
+
if (args.includes("--json")) {
|
|
280345
|
+
const entry = getKeyEntry(mode2);
|
|
280346
|
+
const out = {
|
|
280347
|
+
mode: mode2,
|
|
280348
|
+
keyPrefix: process.env.ABLO_API_KEY ? process.env.ABLO_API_KEY.slice(0, 12) : entry?.apiKey.slice(0, 12) ?? null,
|
|
280349
|
+
keySource: process.env.ABLO_API_KEY ? "env" : entry ? "stored" : null,
|
|
280350
|
+
organizationId: entry?.organizationId ?? null,
|
|
280351
|
+
apiUrl,
|
|
280352
|
+
reachable: await ping(apiUrl)
|
|
280353
|
+
};
|
|
280354
|
+
console.log(JSON.stringify(out, null, 2));
|
|
280355
|
+
return;
|
|
280356
|
+
}
|
|
280343
280357
|
console.log(`
|
|
280344
280358
|
${brand("ablo")} ${import_picocolors9.default.dim("status")}
|
|
280345
280359
|
`);
|
|
@@ -281685,7 +281699,7 @@ async function main() {
|
|
|
281685
281699
|
} else if (command === "mode") {
|
|
281686
281700
|
await mode(process.argv.slice(3));
|
|
281687
281701
|
} else if (command === "status") {
|
|
281688
|
-
await status();
|
|
281702
|
+
await status(process.argv.slice(3));
|
|
281689
281703
|
} else if (command === "logs") {
|
|
281690
281704
|
await logs(process.argv.slice(3));
|
|
281691
281705
|
} else if (command === "webhooks") {
|
|
@@ -281735,6 +281749,7 @@ async function main() {
|
|
|
281735
281749
|
console.log(` npx ablo logout Remove the stored API key`);
|
|
281736
281750
|
console.log(` npx ablo mode [sandbox|production] Switch active environment, like Stripe`);
|
|
281737
281751
|
console.log(` npx ablo status Show org, mode, keys, and server health`);
|
|
281752
|
+
console.log(` npx ablo status --json Same, machine-readable (mode, key prefix, org id, api host)`);
|
|
281738
281753
|
console.log(` npx ablo logs [-n N] [--since 15m] Tail commit activity (follows; --no-follow to exit)`);
|
|
281739
281754
|
console.log(` npx ablo webhooks create <url> Register an outbound webhook endpoint (writes ABLO_WEBHOOK_SECRET)`);
|
|
281740
281755
|
console.log(` npx ablo webhooks list|roll|enable|rm Manage webhook endpoints + delivery health`);
|
package/dist/client/Ablo.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { SyncEngineConfig, SyncLogger, MutationExecutor, MutationDispatcher
|
|
|
23
23
|
import { ObjectPool } from '../ObjectPool.js';
|
|
24
24
|
import type { SyncStoreContract } from '../react/context.js';
|
|
25
25
|
import type { SyncWebSocket } from '../sync/SyncWebSocket.js';
|
|
26
|
+
import type { SyncGroupInput } from '../schema/roles.js';
|
|
26
27
|
import { type SyncStatus } from '../BaseSyncedStore.js';
|
|
27
28
|
import type { IntentStream, IntentWaitOptions, PresenceStream, Snapshot } from '../types/streams.js';
|
|
28
29
|
import type { ParticipantManager } from '../sync/participants.js';
|
|
@@ -577,8 +578,11 @@ export interface CreateUserSessionParams {
|
|
|
577
578
|
user: {
|
|
578
579
|
id: string;
|
|
579
580
|
};
|
|
580
|
-
/** Sync groups this session may subscribe to
|
|
581
|
-
|
|
581
|
+
/** Sync groups this session may subscribe to — typed (`'default'` or
|
|
582
|
+
* `<namespace>:<id>`; build with `syncGroup.org()/user()/of()` from
|
|
583
|
+
* `@abloatai/ablo/schema`). Omit for the server default:
|
|
584
|
+
* `[org:<your org>, user:<user.id>]`. */
|
|
585
|
+
syncGroups?: readonly SyncGroupInput[];
|
|
582
586
|
/** Token lifetime in seconds. Defaults to 900 (15m, the Stripe ephemeral default). */
|
|
583
587
|
ttlSeconds?: number;
|
|
584
588
|
/** Opaque identity blob echoed back to the client as `ablo.user`. */
|
|
@@ -599,8 +603,11 @@ export interface CreateAgentSessionParams<S extends SchemaRecord> {
|
|
|
599
603
|
can: {
|
|
600
604
|
[M in keyof S & string]?: readonly SessionOperation[];
|
|
601
605
|
};
|
|
602
|
-
/** Sync groups this session may subscribe to
|
|
603
|
-
|
|
606
|
+
/** Sync groups this session may subscribe to — typed (`'default'` or
|
|
607
|
+
* `<namespace>:<id>`; build with `syncGroup.org()/user()/of()` from
|
|
608
|
+
* `@abloatai/ablo/schema`). Omit for the server default: the org
|
|
609
|
+
* anchor (`org:<your org>`) + the agent's own anchor. */
|
|
610
|
+
syncGroups?: readonly SyncGroupInput[];
|
|
604
611
|
/** Token lifetime in seconds. Defaults to 900 (15m, the Stripe ephemeral default). */
|
|
605
612
|
ttlSeconds?: number;
|
|
606
613
|
/** Opaque identity blob echoed back to the client as `ablo.agent`. */
|
|
@@ -611,13 +618,14 @@ export interface CreateAgentSessionParams<S extends SchemaRecord> {
|
|
|
611
618
|
* `{ user }` for a full-authority end-user session (`ek_`) or `{ agent, can }`
|
|
612
619
|
* for a scoped agent session (`rk_`). */
|
|
613
620
|
export type CreateSessionParams<S extends SchemaRecord> = CreateUserSessionParams | CreateAgentSessionParams<S>;
|
|
614
|
-
/** A minted
|
|
615
|
-
*
|
|
621
|
+
/** A minted session token — the Stripe ephemeral-key / Supabase session
|
|
622
|
+
* resource. `token` is the secret the holder presents as its bearer. */
|
|
616
623
|
export interface AbloSession {
|
|
617
624
|
object: 'session';
|
|
618
625
|
/** Stable id of the minted credential (for revocation). */
|
|
619
626
|
id: string;
|
|
620
|
-
/** The short-lived
|
|
627
|
+
/** The short-lived session token — `ek_` for a `{ user }` session, `rk_`
|
|
628
|
+
* for an `{ agent }` session. Hand this to the participant's runtime. */
|
|
621
629
|
token: string;
|
|
622
630
|
/** ISO-8601 expiry. */
|
|
623
631
|
expiresAt: string;
|
|
@@ -716,17 +724,29 @@ export type Ablo<S extends SchemaRecord> = {
|
|
|
716
724
|
* BACKEND (where the `sk_` secret key lives), then hand the returned
|
|
717
725
|
* `token` to that user's browser (typically via an authEndpoint the client
|
|
718
726
|
* fetches). The browser presents it as the bearer; the sync-server verifies
|
|
719
|
-
*
|
|
727
|
+
* it via `apiKeyProvider`.
|
|
720
728
|
*
|
|
721
729
|
* The browser must NEVER see the `sk_` key — only the per-user session token.
|
|
722
730
|
*
|
|
723
|
-
* Pass `{ user: { id } }` for a full-authority end-user session (mints `ek_
|
|
724
|
-
* or `{ agent: { id }, can: {
|
|
725
|
-
* session (mints `rk_`); `can` is typed
|
|
731
|
+
* Pass `{ user: { id } }` for a full-authority end-user session (mints `ek_`,
|
|
732
|
+
* `actor_kind: 'user'` attribution), or `{ agent: { id }, can: { tasks:
|
|
733
|
+
* ['update'] } }` for a scoped agent session (mints `rk_`); `can` is typed
|
|
734
|
+
* against your schema's model names. Always authenticates with the original
|
|
735
|
+
* `sk_` — never the client's exchanged sync credential.
|
|
726
736
|
*/
|
|
727
737
|
sessions: {
|
|
728
738
|
create(params: CreateSessionParams<S>): Promise<AbloSession>;
|
|
729
739
|
};
|
|
740
|
+
/**
|
|
741
|
+
* The organization this client resolved to — `null` until `ready()`
|
|
742
|
+
* completes. Use it instead of scraping CLI output or hardcoding env vars:
|
|
743
|
+
*
|
|
744
|
+
* ```ts
|
|
745
|
+
* await ablo.ready();
|
|
746
|
+
* const org = ablo.organizationId; // 'org_…'
|
|
747
|
+
* ```
|
|
748
|
+
*/
|
|
749
|
+
readonly organizationId: string | null;
|
|
730
750
|
/**
|
|
731
751
|
* Destroy every IndexedDB database owned by this engine. Disconnects
|
|
732
752
|
* the WebSocket, releases timers, and deletes all `ablo_*` / `ablo-*`
|
|
@@ -810,16 +830,6 @@ export type Ablo<S extends SchemaRecord> = {
|
|
|
810
830
|
* connection is rotated on `dispose()` but this object is the same.
|
|
811
831
|
*/
|
|
812
832
|
readonly presence: PresenceStream;
|
|
813
|
-
/**
|
|
814
|
-
* @internal — the public coordination API is `ablo.<model>.claim`. This
|
|
815
|
-
* accessor is the internal stream `claim` is built on; it is NOT part of the
|
|
816
|
-
* supported public surface and will be moved off the public type (it currently
|
|
817
|
-
* stays only because internal SDK modules are still typed against it).
|
|
818
|
-
*
|
|
819
|
-
* Cooperative-mutex layer over presence — announce "I'm about to do X on Y" so
|
|
820
|
-
* peers can yield before colliding. Same socket as entity sync.
|
|
821
|
-
*/
|
|
822
|
-
readonly intents: IntentResource;
|
|
823
833
|
/**
|
|
824
834
|
* Canonical low-level mutation API. Every untyped model write compiles
|
|
825
835
|
* down to `commits.create(...)`.
|
package/dist/client/Ablo.js
CHANGED
|
@@ -25,7 +25,7 @@ import { initSyncEngine } from '../context.js';
|
|
|
25
25
|
import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../SyncEngineContext.js';
|
|
26
26
|
import { alwaysOnline } from '../adapters/alwaysOnline.js';
|
|
27
27
|
import { validateAbloOptions } from './validateAbloOptions.js';
|
|
28
|
-
import { exchangeApiKey } from '../auth/index.js';
|
|
28
|
+
import { exchangeApiKey, mintUserSessionKey } from '../auth/index.js';
|
|
29
29
|
import { createAuthCredentialSource } from '../auth/credentialSource.js';
|
|
30
30
|
import { createInternalComponents } from './createInternalComponents.js';
|
|
31
31
|
import { resolveParticipantIdentity } from './identity.js';
|
|
@@ -860,6 +860,9 @@ export function Ablo(options) {
|
|
|
860
860
|
// source of truth. No duplicate closure variables.
|
|
861
861
|
let _readyPromise = null;
|
|
862
862
|
let _refreshScheduler = null;
|
|
863
|
+
/** Resolved account scope — set once identity resolution completes in
|
|
864
|
+
* `ready()`; exposed as the readonly `ablo.organizationId` accessor. */
|
|
865
|
+
let _resolvedOrganizationId = null;
|
|
863
866
|
async function ready() {
|
|
864
867
|
if (_readyPromise)
|
|
865
868
|
return _readyPromise;
|
|
@@ -942,6 +945,7 @@ export function Ablo(options) {
|
|
|
942
945
|
'`["org:${orgId}", "user:${userId}"]`) or verify your auth ' +
|
|
943
946
|
'provider populates them. See packages/sync-engine/src/client/identity.ts.', { participantKind, resolvedSyncGroups });
|
|
944
947
|
}
|
|
948
|
+
_resolvedOrganizationId = accountScope;
|
|
945
949
|
if (resolved.refreshScheduler) {
|
|
946
950
|
_refreshScheduler = resolved.refreshScheduler;
|
|
947
951
|
}
|
|
@@ -1505,6 +1509,16 @@ export function Ablo(options) {
|
|
|
1505
1509
|
},
|
|
1506
1510
|
};
|
|
1507
1511
|
}
|
|
1512
|
+
/**
|
|
1513
|
+
* The CONTROL-PLANE credential: always the original configured secret key.
|
|
1514
|
+
* Never reads `authCredentials` — that holds the exchanged sync credential
|
|
1515
|
+
* (a wide-scope `rk_` on the hosted path), which control-plane routes
|
|
1516
|
+
* rightly refuse (e.g. the user-session mint is sk_-gated). Counterpart to
|
|
1517
|
+
* `getAuthToken()`, which resolves the sync-plane token.
|
|
1518
|
+
*/
|
|
1519
|
+
async function controlPlaneApiKey() {
|
|
1520
|
+
return resolveApiKeyValue(configuredApiKey);
|
|
1521
|
+
}
|
|
1508
1522
|
const engine = {
|
|
1509
1523
|
...modelProxies,
|
|
1510
1524
|
ready,
|
|
@@ -1523,6 +1537,13 @@ export function Ablo(options) {
|
|
|
1523
1537
|
async getAuthToken() {
|
|
1524
1538
|
// The live short-lived bearer (set via `setAuthToken`/`getToken` refresh)
|
|
1525
1539
|
// is the canonical credential; fall back to a configured API key.
|
|
1540
|
+
//
|
|
1541
|
+
// This is the SYNC-PLANE token (bootstrap, WS, query HTTP). Control-plane
|
|
1542
|
+
// calls (sessions.create, datasource registration) never use it — they
|
|
1543
|
+
// present the ORIGINAL secret key via `controlPlaneApiKey()` below. The
|
|
1544
|
+
// split matters: after the startup exchange this resolver returns the
|
|
1545
|
+
// derived wide-scope `rk_`, a credential the control-plane routes
|
|
1546
|
+
// correctly refuse (an agent token must never mint humans).
|
|
1526
1547
|
return (authCredentials.getAuthToken() ??
|
|
1527
1548
|
(await resolveApiKeyValue(configuredApiKey)) ??
|
|
1528
1549
|
configuredAuthToken ??
|
|
@@ -1531,15 +1552,26 @@ export function Ablo(options) {
|
|
|
1531
1552
|
setCredentialRefresher(refresher) {
|
|
1532
1553
|
store.setCredentialRefresher(refresher);
|
|
1533
1554
|
},
|
|
1555
|
+
// The org this client resolved to — null until `ready()` completes.
|
|
1556
|
+
// Integrators previously had no programmatic way to learn it (the Pulse
|
|
1557
|
+
// agent regex-scraped `ablo status` output); now it's a property.
|
|
1558
|
+
get organizationId() {
|
|
1559
|
+
return _resolvedOrganizationId;
|
|
1560
|
+
},
|
|
1534
1561
|
nudgeReconnect() {
|
|
1535
1562
|
store.nudgeReconnect();
|
|
1536
1563
|
},
|
|
1537
1564
|
sessions: {
|
|
1538
1565
|
// Stripe `ephemeralKeys.create` shape: a BACKEND (holding `sk_`) mints a
|
|
1539
|
-
// short-lived scoped token for one end user OR one agent.
|
|
1540
|
-
//
|
|
1566
|
+
// short-lived scoped token for one end user OR one agent.
|
|
1567
|
+
//
|
|
1568
|
+
// CONTROL-PLANE CREDENTIAL RULE: both arms authenticate with the
|
|
1569
|
+
// ORIGINAL secret key (`controlPlaneApiKey()`), never the wide-scope
|
|
1570
|
+
// `rk_` the startup exchange installed as the sync credential. A derived
|
|
1571
|
+
// agent credential silently replacing the secret key on control-plane
|
|
1572
|
+
// calls is how humans get minted as agents — attribution is the product.
|
|
1541
1573
|
async create(params) {
|
|
1542
|
-
const apiKey = await
|
|
1574
|
+
const apiKey = await controlPlaneApiKey();
|
|
1543
1575
|
if (!apiKey) {
|
|
1544
1576
|
throw new AbloAuthenticationError('sessions.create requires a secret (sk_) API key — call it from your backend, not the browser.', { code: 'apikey_missing' });
|
|
1545
1577
|
}
|
|
@@ -1547,30 +1579,53 @@ export function Ablo(options) {
|
|
|
1547
1579
|
url,
|
|
1548
1580
|
bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
|
|
1549
1581
|
});
|
|
1550
|
-
// Discriminate the union
|
|
1551
|
-
//
|
|
1552
|
-
//
|
|
1553
|
-
//
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1582
|
+
// Discriminate the union onto the server's TWO mint doors:
|
|
1583
|
+
// `{ user }` → POST /auth/ephemeral-keys → `ek_` (sk_-gated; the
|
|
1584
|
+
// user-session door). Routing this arm through
|
|
1585
|
+
// /auth/capability is structurally impossible — that
|
|
1586
|
+
// route rejects participantKind 'user' outright
|
|
1587
|
+
// (`invalid_participant_kind`, the 2026-06-11 Pulse
|
|
1588
|
+
// cascade: the SDK's own blessed pattern 403'd and
|
|
1589
|
+
// integrators fell back to minting humans as agents).
|
|
1590
|
+
// `{ agent }` → POST /auth/capability → scoped `rk_`.
|
|
1591
|
+
// `can: { tasks: ['update'] }` serializes to the wire
|
|
1592
|
+
// allowlist (`tasks.update`); the Hub matches it
|
|
1593
|
+
// against every registered alias of the model.
|
|
1557
1594
|
if (params.user) {
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1595
|
+
const res = await mintUserSessionKey({
|
|
1596
|
+
apiKey,
|
|
1597
|
+
baseUrl,
|
|
1598
|
+
userId: params.user.id,
|
|
1599
|
+
...(params.syncGroups ? { syncGroups: [...params.syncGroups] } : {}),
|
|
1600
|
+
ttlSeconds: params.ttlSeconds ?? 900,
|
|
1601
|
+
...(internalOptions.fetch ? { fetch: internalOptions.fetch } : {}),
|
|
1602
|
+
});
|
|
1603
|
+
return {
|
|
1604
|
+
object: 'session',
|
|
1605
|
+
id: res.id,
|
|
1606
|
+
token: res.token,
|
|
1607
|
+
expiresAt: res.expiresAt,
|
|
1608
|
+
organizationId: res.organizationId,
|
|
1609
|
+
// The ephemeral mint stores scope on the key row; reshape its flat
|
|
1610
|
+
// response into the session resource's scope block.
|
|
1611
|
+
scope: {
|
|
1612
|
+
organizationId: res.organizationId,
|
|
1613
|
+
syncGroups: res.syncGroups,
|
|
1614
|
+
operations: [],
|
|
1615
|
+
participantKind: 'user',
|
|
1616
|
+
participantId: res.participantId,
|
|
1617
|
+
},
|
|
1618
|
+
userMeta: params.userMeta ?? { id: res.participantId },
|
|
1619
|
+
};
|
|
1566
1620
|
}
|
|
1621
|
+
const operations = Object.entries(params.can).flatMap(([model, ops]) => (ops ?? []).map((op) => `${model.toLowerCase()}.${op}`));
|
|
1567
1622
|
const res = await exchangeApiKey({
|
|
1568
1623
|
apiKey,
|
|
1569
1624
|
baseUrl,
|
|
1570
|
-
participantKind,
|
|
1571
|
-
participantId,
|
|
1625
|
+
participantKind: 'agent',
|
|
1626
|
+
participantId: params.agent.id,
|
|
1572
1627
|
...(params.syncGroups ? { syncGroups: [...params.syncGroups] } : {}),
|
|
1573
|
-
|
|
1628
|
+
operations,
|
|
1574
1629
|
ttlSeconds: params.ttlSeconds ?? 900,
|
|
1575
1630
|
...(params.userMeta ? { userMeta: params.userMeta } : {}),
|
|
1576
1631
|
...(internalOptions.fetch ? { fetch: internalOptions.fetch } : {}),
|
package/dist/errorCodes.d.ts
CHANGED
|
@@ -316,6 +316,7 @@ export declare const ERROR_CODES: {
|
|
|
316
316
|
readonly invalid_request: ErrorCodeSpec;
|
|
317
317
|
readonly capability_not_found: ErrorCodeSpec;
|
|
318
318
|
readonly invalid_participant_kind: ErrorCodeSpec;
|
|
319
|
+
readonly invalid_sync_group: ErrorCodeSpec;
|
|
319
320
|
readonly narrow_scope_required: ErrorCodeSpec;
|
|
320
321
|
readonly wide_scope_forbidden: ErrorCodeSpec;
|
|
321
322
|
readonly capability_required: ErrorCodeSpec;
|
package/dist/errorCodes.js
CHANGED
|
@@ -338,6 +338,7 @@ export const ERROR_CODES = {
|
|
|
338
338
|
invalid_request: wire('validation', 400, false, 'The request parameters were invalid.'),
|
|
339
339
|
capability_not_found: wire('not_found', 404, false, 'No capability exists with the given id.'),
|
|
340
340
|
invalid_participant_kind: wire('validation', 400, false, 'The participant kind is invalid.'),
|
|
341
|
+
invalid_sync_group: wire('validation', 400, false, 'Sync groups must be "default" or "<namespace>:<id>".'),
|
|
341
342
|
narrow_scope_required: wire('validation', 400, false, 'A narrowed scope is required for this request.'),
|
|
342
343
|
wide_scope_forbidden: wire('permission', 403, false, 'A wide scope is not permitted for this caller.'),
|
|
343
344
|
capability_required: wire('auth', 401, false, 'This operation requires a capability.'),
|
|
@@ -88,8 +88,6 @@ export interface AbloProviderProps<R extends SchemaRecord = SchemaRecord> {
|
|
|
88
88
|
* Sentry/Datadog. React-only consumers can use `useErrorListener()` instead.
|
|
89
89
|
*/
|
|
90
90
|
onError?: (error: Error) => void;
|
|
91
|
-
/** @internal placeholder so the old WS-URL prop shape doesn't silently leak in. */
|
|
92
|
-
url?: never;
|
|
93
91
|
/**
|
|
94
92
|
* Rendered in place of `children` during the *first* bootstrap pass —
|
|
95
93
|
* while the engine is actively transitioning from `initial` →
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* }),
|
|
18
18
|
* });
|
|
19
19
|
*
|
|
20
|
-
* type Task =
|
|
20
|
+
* type Task = Model<typeof schema, 'tasks'>;
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
export { z } from 'zod';
|
|
@@ -29,7 +29,7 @@ export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syn
|
|
|
29
29
|
export { syncDeltaActionSchema, wireDeltaDataSchema, participantRefSchema, syncDeltaWireCoreSchema, clientSyncDeltaSchema, serverSyncDeltaSchema, type SyncDeltaAction, type WireDeltaData, type ParticipantRef, type SyncDeltaWireCore, type ClientSyncDelta, type ServerSyncDelta, } from './sync-delta-wire.js';
|
|
30
30
|
export { model, scopeKindOf, type ModelDef, type ModelOptions, type LoadStrategy, type PersistOptions, type RelationRecord, type GrantsRef, } from './model.js';
|
|
31
31
|
export { mutable, readOnly, type SugarOptions } from './sugar.js';
|
|
32
|
-
export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './schema.js';
|
|
32
|
+
export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type Model, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './schema.js';
|
|
33
33
|
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, type SchemaJSON, type ModelJSON, type RelationJSON, } from './serialize.js';
|
|
34
34
|
export { selectModels } from './select.js';
|
|
35
35
|
export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, snakeToCamel, q, sqlType, type ProvisionPlan, type MigrationPlan, } from './ddl.js';
|
package/dist/schema/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* }),
|
|
18
18
|
* });
|
|
19
19
|
*
|
|
20
|
-
* type Task =
|
|
20
|
+
* type Task = Model<typeof schema, 'tasks'>;
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
// Re-export Zod for convenience (consumers can also import directly)
|
|
@@ -46,7 +46,7 @@ export { model, scopeKindOf, } from './model.js';
|
|
|
46
46
|
// falls back to sensible defaults. See sugar.ts for the full pattern.
|
|
47
47
|
export { mutable, readOnly } from './sugar.js';
|
|
48
48
|
// Schema definition + type inference
|
|
49
|
-
export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './schema.js';
|
|
49
|
+
export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './schema.js';
|
|
50
50
|
// Schema ⇄ JSON (control-plane transport for hosted multi-tenant)
|
|
51
51
|
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, } from './serialize.js';
|
|
52
52
|
// Schema projection — derive an app's subset from one canonical schema.
|
package/dist/schema/roles.d.ts
CHANGED
|
@@ -35,6 +35,24 @@ export type SyncGroup = z.infer<typeof syncGroupSchema>;
|
|
|
35
35
|
* it changes here and nowhere else.
|
|
36
36
|
*/
|
|
37
37
|
export declare function syncGroup(kind: string, id: string): SyncGroup;
|
|
38
|
+
/**
|
|
39
|
+
* Caller-facing input form of a sync group. Accepts a constructor-minted
|
|
40
|
+
* {@link SyncGroup}, a contextually-typed template literal of the right shape
|
|
41
|
+
* (`` `org:${orgId}` `` checks without importing the constructor), or the
|
|
42
|
+
* server-reserved `'default'` anchor. A bare colon-less string is a COMPILE
|
|
43
|
+
* error — it would subscribe to nothing and fail silently (the `['default']`
|
|
44
|
+
* zero-fan-out ghost made flesh).
|
|
45
|
+
*/
|
|
46
|
+
export type SyncGroupInput = SyncGroup | `${string}:${string}` | 'default';
|
|
47
|
+
/**
|
|
48
|
+
* Runtime gate for {@link SyncGroupInput} at parse boundaries (capability
|
|
49
|
+
* mint, ephemeral-key mint). One schema, every door — a malformed group is
|
|
50
|
+
* rejected loudly (`invalid_sync_group`) instead of stored and silently
|
|
51
|
+
* subscribed-to-nothing.
|
|
52
|
+
*/
|
|
53
|
+
export declare const syncGroupInputSchema: z.ZodUnion<readonly [z.ZodLiteral<"default">, z.core.$ZodBranded<z.ZodTemplateLiteral<`${string}:${string}`>, "SyncGroup", "out">]>;
|
|
54
|
+
/** Runtime guard matching {@link SyncGroupInput}. */
|
|
55
|
+
export declare function isSyncGroupInput(value: unknown): value is SyncGroupInput;
|
|
38
56
|
/** Validates how a role pulls ids out of a context (identity or record). */
|
|
39
57
|
export declare const roleSourceSchema: z.ZodObject<{
|
|
40
58
|
field: z.ZodString;
|
package/dist/schema/roles.js
CHANGED
|
@@ -39,6 +39,17 @@ export const syncGroupSchema = z
|
|
|
39
39
|
export function syncGroup(kind, id) {
|
|
40
40
|
return `${kind}:${id}`;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Runtime gate for {@link SyncGroupInput} at parse boundaries (capability
|
|
44
|
+
* mint, ephemeral-key mint). One schema, every door — a malformed group is
|
|
45
|
+
* rejected loudly (`invalid_sync_group`) instead of stored and silently
|
|
46
|
+
* subscribed-to-nothing.
|
|
47
|
+
*/
|
|
48
|
+
export const syncGroupInputSchema = z.union([z.literal('default'), syncGroupSchema]);
|
|
49
|
+
/** Runtime guard matching {@link SyncGroupInput}. */
|
|
50
|
+
export function isSyncGroupInput(value) {
|
|
51
|
+
return syncGroupInputSchema.safeParse(value).success;
|
|
52
|
+
}
|
|
42
53
|
// ── Role source ─────────────────────────────────────────────────────────────
|
|
43
54
|
/** Validates how a role pulls ids out of a context (identity or record). */
|
|
44
55
|
export const roleSourceSchema = z.object({
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { z } from 'zod';
|
|
|
23
23
|
import type { ModelDef, RelationRecord } from './model.js';
|
|
24
24
|
import type { RelationDef } from './relation.js';
|
|
25
25
|
import type { IdentityRole } from './roles.js';
|
|
26
|
-
export { type IdentityRole, type IdentityRoleSource, type IdentityContext, type EntityRole, type EntityRoleSource, type EntityContext, type RoleSource, type RoleContext, type SyncGroup, identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './roles.js';
|
|
26
|
+
export { type IdentityRole, type IdentityRoleSource, type IdentityContext, type EntityRole, type EntityRoleSource, type EntityContext, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './roles.js';
|
|
27
27
|
/** The set of built-in casing conventions supported by `defineSchema`. */
|
|
28
28
|
export type CasingConvention = 'snake_case' | 'camelCase';
|
|
29
29
|
/** Plug point for custom conventions (e.g. mixed legacy databases). */
|
|
@@ -115,6 +115,29 @@ export interface Schema<S extends SchemaRecord = SchemaRecord> {
|
|
|
115
115
|
* type Task = InferModel<typeof schema, 'tasks'>;
|
|
116
116
|
* ```
|
|
117
117
|
*/
|
|
118
|
+
/** The schema bound via `declare module … interface Register { Schema: … }`
|
|
119
|
+
* (the `ablo.d.ts` the scaffold writes). `never` when not registered. */
|
|
120
|
+
type RegisteredSchema = import('../types/global.js').Register extends {
|
|
121
|
+
Schema: infer S extends Schema;
|
|
122
|
+
} ? S : never;
|
|
123
|
+
/**
|
|
124
|
+
* THE model type helper. With the scaffold's `ablo.d.ts` registration in
|
|
125
|
+
* place, one parameter is all it takes:
|
|
126
|
+
*
|
|
127
|
+
* ```ts
|
|
128
|
+
* type Task = Model<'tasks'>;
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* Without registration (or for a second schema), pass the schema explicitly:
|
|
132
|
+
* `Model<typeof schema, 'tasks'>`.
|
|
133
|
+
*/
|
|
134
|
+
export type Model<A, B = never> = [B] extends [never] ? A extends keyof RegisteredSchema['models'] ? InferModel<RegisteredSchema, A> : never : A extends Schema ? InferModel<A, B extends keyof A['models'] ? B : never> : never;
|
|
135
|
+
/**
|
|
136
|
+
* @deprecated Use {@link Model} — `type Task = Model<typeof schema, 'tasks'>`
|
|
137
|
+
* reads as the domain ("the Task model from my schema"), not the machinery.
|
|
138
|
+
* Drizzle deprecated its own `InferModel` for the same reason. Kept as an
|
|
139
|
+
* alias; no behavior difference.
|
|
140
|
+
*/
|
|
118
141
|
export type InferModel<S extends Schema, ModelName extends keyof S['models']> = S['models'][ModelName] extends ModelDef<infer Shape, infer R, infer C> ? z.infer<z.ZodObject<Shape>> & BaseModelFields & BaseModelMethods & InferComputed<C> & InferRelations<S, R> : never;
|
|
119
142
|
/**
|
|
120
143
|
* Infer relation accessor types from a model's relations record.
|
package/dist/schema/schema.js
CHANGED
|
@@ -25,7 +25,7 @@ import { scopeSchema, grantsRefSchema } from './roles.js';
|
|
|
25
25
|
// Sync-group roles (identity + entity) live in `./roles.js`. Re-exported here
|
|
26
26
|
// so the long-standing `@ablo/schema` / `./schema.js` import paths keep working
|
|
27
27
|
// after the rehome — see roles.ts for the full vocabulary.
|
|
28
|
-
export { identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './roles.js';
|
|
28
|
+
export { identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './roles.js';
|
|
29
29
|
function resolveCasing(fn) {
|
|
30
30
|
if (fn === undefined)
|
|
31
31
|
return (x) => x;
|
|
@@ -1,24 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* `@abloatai/ablo/server` — the storage-mode vocabulary the `DataAdapter`
|
|
3
|
-
* contract supports. Analogous to Better Auth's adapter `id`/`adapterId`: a
|
|
4
|
-
* diagnostic discriminator on the adapter, NOT a routing switch (routing goes
|
|
5
|
-
* through the resolver/factory). The package owns this enum so the contract and
|
|
6
|
-
* every host adapter agree on the closed set:
|
|
7
|
-
* - `hosted` — Ablo's control-plane database.
|
|
8
|
-
* - `selfHosted` — the customer's database, same execution path as hosted.
|
|
9
|
-
* - `source` — a customer-owned endpoint (credentialless ingestion).
|
|
10
|
-
*
|
|
11
|
-
* @internal Deployment topology, not product vocabulary. Customers never see a
|
|
12
|
-
* "storage mode" — their story is `Ablo({ schema, apiKey, databaseUrl })` and
|
|
13
|
-
* one `datasource` resource (docs/plans/sync-engine-stripe-story-scope.md).
|
|
14
|
-
* This export exists for the sync-server host only.
|
|
15
|
-
*/
|
|
16
|
-
import { z } from 'zod';
|
|
17
|
-
/** @internal See module note — host-deployment vocabulary, never customer-facing. */
|
|
18
|
-
export declare const storageModeSchema: z.ZodEnum<{
|
|
19
|
-
source: "source";
|
|
20
|
-
hosted: "hosted";
|
|
21
|
-
selfHosted: "selfHosted";
|
|
22
|
-
}>;
|
|
23
|
-
/** @internal See module note — host-deployment vocabulary, never customer-facing. */
|
|
24
|
-
export type StorageMode = z.infer<typeof storageModeSchema>;
|
|
1
|
+
export {};
|
package/docs/api-keys.md
CHANGED
|
@@ -23,6 +23,22 @@ Use API keys from trusted (server-side) runtimes:
|
|
|
23
23
|
|
|
24
24
|
Never ship a secret API key to a browser bundle.
|
|
25
25
|
|
|
26
|
+
## Publishable key (`pk_`) — browser-safe, read-only
|
|
27
|
+
|
|
28
|
+
For a read-only browser experience, a publishable key is safe to ship in the
|
|
29
|
+
bundle. Like a Stripe `pk_` or a Supabase anon key, it is long-lived,
|
|
30
|
+
org-scoped, and used **directly as the bearer** — never exchanged, never
|
|
31
|
+
expires, nothing to refresh:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const ablo = Ablo({ apiKey: process.env.NEXT_PUBLIC_ABLO_PUBLISHABLE_KEY }); // pk_live_…
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A `pk_` grants **read-only** access to the org's data plane: it cannot write and
|
|
38
|
+
cannot reach any control-plane operation. The moment the browser needs to write
|
|
39
|
+
on a specific user's behalf, mint a short-lived `ek_` user session from your
|
|
40
|
+
backend instead (see the Sessions guide).
|
|
41
|
+
|
|
26
42
|
## Sandboxes and production
|
|
27
43
|
|
|
28
44
|
Test and live keys are the same shape; the prefix names the environment:
|
package/docs/quickstart.md
CHANGED
|
@@ -50,6 +50,29 @@ export const schema = defineSchema({
|
|
|
50
50
|
});
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
|
|
54
|
+
Register the schema once (init scaffolds this `ablo.d.ts`), and every type
|
|
55
|
+
is one parameter away — no `typeof schema` re-stating, anywhere:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// ablo.d.ts — once per project
|
|
59
|
+
import type { schema } from './ablo/schema';
|
|
60
|
+
declare module '@abloatai/ablo' {
|
|
61
|
+
interface Register { Schema: typeof schema }
|
|
62
|
+
}
|
|
63
|
+
export {};
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import type { Model } from '@abloatai/ablo/schema';
|
|
68
|
+
|
|
69
|
+
type WeatherReport = Model<'weatherReports'>; // fully typed from YOUR schema
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
(The same `Register` binding types every hook and client — it's the
|
|
73
|
+
TanStack-Router pattern: declare the source of truth once, everything
|
|
74
|
+
infers from it.)
|
|
75
|
+
|
|
53
76
|
## 3. Point Ablo at your database
|
|
54
77
|
|
|
55
78
|
The client takes your schema, your key, and your `DATABASE_URL`. On first
|
package/llms-full.txt
CHANGED
|
@@ -19,6 +19,9 @@ Public imports:
|
|
|
19
19
|
- `@abloatai/ablo/react` — React provider and hooks.
|
|
20
20
|
- `@abloatai/ablo/testing` — test harnesses and mocks.
|
|
21
21
|
|
|
22
|
+
TYPES: have the project register its schema ONCE via declaration merging (init scaffolds `ablo.d.ts`): `declare module '@abloatai/ablo' { interface Register { Schema: typeof schema } }`. 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.
|
|
23
|
+
|
|
24
|
+
|
|
22
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).
|
|
23
26
|
|
|
24
27
|
The canonical integration doc is `integration-guide`. It explains the end-to-end
|
package/llms.txt
CHANGED
|
@@ -20,6 +20,9 @@ Then PUSH — this is the step everything depends on. The server keeps its OWN c
|
|
|
20
20
|
import Ablo from '@abloatai/ablo';
|
|
21
21
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
22
22
|
|
|
23
|
+
TYPES: have the project register its schema ONCE via declaration merging (init scaffolds `ablo.d.ts`): `declare module '@abloatai/ablo' { interface Register { Schema: typeof schema } }`. 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.
|
|
24
|
+
|
|
25
|
+
|
|
23
26
|
const schema = defineSchema({
|
|
24
27
|
weatherReports: model({
|
|
25
28
|
id: z.string(),
|