@abloatai/ablo 0.9.10 → 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 CHANGED
@@ -1,10 +1,21 @@
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
+
3
14
  ## 0.9.10
4
15
 
5
16
  ### Patch Changes
6
17
 
7
- - README: add a centered brand header (Ablo banner, tagline, Docs/Quickstart/Self-host/API/GitHub nav, and status badges).
18
+ - README: add a centered brand header (Ablo banner, tagline, doc nav links, and status badges).
8
19
 
9
20
  ## 0.9.9
10
21
 
package/README.md CHANGED
@@ -7,10 +7,9 @@
7
7
  </p>
8
8
 
9
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/data-sources">Self-host</a> ·
13
- <a href="https://abloatai.com/api">API</a> ·
10
+ <a href="https://abloatai.com">Docs</a> &nbsp;|&nbsp;
11
+ <a href="https://abloatai.com/quickstart">Quickstart</a> &nbsp;|&nbsp;
12
+ <a href="https://abloatai.com/api">API</a> &nbsp;|&nbsp;
14
13
  <a href="https://github.com/Abloatai/ablo">GitHub</a>
15
14
  </p>
16
15
 
@@ -36,14 +35,11 @@ agent claims the row. If someone else is already working on it, `claim` waits,
36
35
  re-reads the fresh row, then hands it over. No stale overwrite, no separate
37
36
  agent mutation path.
38
37
 
39
- Under the hood, you define your data once with a Zod schema and get the same
40
- typed model client for every actor — people, server actions, and agents:
38
+ Under the hood, you define a Zod schema once and get typed model clients for
39
+ every actor:
41
40
 
42
- ```ts
43
- await ablo.task.create({ data }) // create
44
- await ablo.task.retrieve({ id }) // read
45
- await ablo.task.update({ id, data }) // update
46
- await using task = await ablo.task.claim({ id }) // claim for safe, slow agent work
41
+ ```txt
42
+ schema -> ablo.<model>.create/retrieve/update/claim(...)
47
43
  ```
48
44
 
49
45
  The schema is the public contract. It gives you typed model methods, realtime
@@ -51,9 +47,8 @@ fanout, React selectors, agent writes, and the HTTP/Data Source shape for
51
47
  non-JavaScript services. Every confirmed change shows up everywhere, and active
52
48
  claims are visible while the work is still in progress.
53
49
 
54
- **[Get started](#set-up)** &nbsp;·&nbsp; point your coding agent at the shipped
55
- `llms.txt` &nbsp;·&nbsp; **upgrading?** see the
56
- [Version History &amp; Migration Guide](./docs/migration.md)
50
+ [Get started](#quick-start) · point your coding agent at the shipped `llms.txt`
51
+ · **upgrading?** see the [Version History & Migration Guide](./docs/migration.md)
57
52
 
58
53
  It works with the auth and database you already have. **Your database is the
59
54
  system of record — Ablo never hosts your data.** Ablo is the transaction layer
@@ -106,6 +101,29 @@ instead of guessing:
106
101
  import Ablo from '@abloatai/ablo';
107
102
  import { defineSchema, model, z } from '@abloatai/ablo/schema';
108
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
+
109
127
  const schema = defineSchema({
110
128
  weatherReports: model({
111
129
  location: z.string(),
@@ -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;
@@ -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
@@ -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;
@@ -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("sync engine \u2014 dev")} ${import_picocolors6.default.dim("(sandbox)")}
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 dev")}, or ${import_picocolors7.default.bold("ablo mode production")} to switch.`);
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`);
@@ -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. Omit to inherit the key's scope. */
581
- syncGroups?: readonly string[];
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. Omit to inherit the key's scope. */
603
- syncGroups?: readonly string[];
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 end-user session token — the Stripe ephemeral-key / Supabase
615
- * session resource. `token` is the secret the browser presents as its bearer. */
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 `rk_` session token. Hand this to the user's browser. */
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
- * the scoped `rk_` token via `apiKeyProvider`.
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: { Task: ['update'] } }` for a scoped agent
725
- * session (mints `rk_`); `can` is typed against your schema's model names.
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(...)`.
@@ -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. Thin wrapper over
1540
- // the `/auth/capability` exchange, reshaped to a Stripe-style resource.
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 resolveApiKeyValue(configuredApiKey);
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: `{ user }` full-authority `ek_` (no op
1551
- // allowlist); `{ agent, can }` scoped `rk_`. `can: { Task: ['update'] }`
1552
- // serializes to the wire allowlist `['task.update']` the Hub matches
1553
- // `${model.toLowerCase()}.${op}` (Hub.ts handleCommit).
1554
- let participantKind;
1555
- let participantId;
1556
- let operations;
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
- participantKind = 'user';
1559
- participantId = params.user.id;
1560
- operations = undefined;
1561
- }
1562
- else {
1563
- participantKind = 'agent';
1564
- participantId = params.agent.id;
1565
- operations = Object.entries(params.can).flatMap(([model, ops]) => (ops ?? []).map((op) => `${model.toLowerCase()}.${op}`));
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
- ...(operations ? { operations } : {}),
1628
+ operations,
1574
1629
  ttlSeconds: params.ttlSeconds ?? 900,
1575
1630
  ...(params.userMeta ? { userMeta: params.userMeta } : {}),
1576
1631
  ...(internalOptions.fetch ? { fetch: internalOptions.fetch } : {}),
@@ -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;
@@ -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` →
@@ -17,7 +17,7 @@
17
17
  * }),
18
18
  * });
19
19
  *
20
- * type Task = InferModel<typeof schema, 'tasks'>;
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';
@@ -17,7 +17,7 @@
17
17
  * }),
18
18
  * });
19
19
  *
20
- * type Task = InferModel<typeof schema, 'tasks'>;
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.
@@ -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;
@@ -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({
@@ -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.
@@ -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 {};
@@ -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(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "description": "State control API for AI agents and collaborative apps.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",