@abloatai/ablo 0.16.1 → 0.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.2
4
+
5
+ ### Patch Changes
6
+
7
+ - **`mintUserSessionKey`: name the shared-schema binding around the project.** The
8
+ two flat options added in 0.16.0 (`schemaOwnerOrgId` + `schemaProjectId`) are
9
+ replaced by one project-centric option — `schemaProject: { organizationId, projectId }` —
10
+ naming "the project that owns the schema" as a single concept. The wire format
11
+ is unchanged (the SDK still sends the same keys), so no server redeploy is
12
+ needed. Released as a patch: the replaced options shipped in 0.16.0 and have no
13
+ external consumers yet.
14
+
15
+ ```ts
16
+ // before
17
+ mintUserSessionKey({ organizationId, schemaOwnerOrgId, schemaProjectId, ... });
18
+ // after
19
+ mintUserSessionKey({
20
+ organizationId, // data org
21
+ schemaProject: { organizationId, projectId }, // the project that owns the schema
22
+ ...
23
+ });
24
+ ```
25
+
3
26
  ## 0.16.1
4
27
 
5
28
  ### Patch Changes
@@ -10,7 +33,7 @@
10
33
  `ABLO_AUTH_URL`), while the human approval page (`/cli`), sign-up, and the
11
34
  key-handoff route (`/api/cli/provision-key`) go to the dashboard host
12
35
  (`www.abloatai.com`, new override `ABLO_DASHBOARD_URL`). Previously every step
13
- ran against `www`, where the Better Auth device endpoints no longer resolve —
36
+ ran against `www`, where the device endpoints no longer resolve —
14
37
  producing "Couldn't start login… Is the dashboard reachable?". The CLI now also
15
38
  builds the approval URL itself rather than trusting the server's
16
39
  `verification_uri`, which (being a relative `/cli`) resolved against the auth
@@ -38,13 +38,18 @@ export interface MintUserSessionRequest {
38
38
  * `Stripe-Account` analogue. Requires the `sk_` to carry
39
39
  * `ephemeral:mint-any-org`; omit to mint into the key's own org. */
40
40
  readonly organizationId?: string;
41
- /** FIRST-PARTY SHARED SCHEMA: bind the minted `ek_` to a schema owner-org +
42
- * project so SCHEMA resolves org-independently (one schema for all the
43
- * integrator's end-user orgs), while DATA stays scoped to `organizationId`.
44
- * Requires the `sk_` to carry `ephemeral:mint-any-org`. Omit for the normal
45
- * per-org behaviour (every external BYO customer). */
46
- readonly schemaProjectId?: string;
47
- readonly schemaOwnerOrgId?: string;
41
+ /** SHARED SCHEMA point this session's SCHEMA at the project that owns it,
42
+ * while its DATA stays scoped to `organizationId`. Use this for org-per-customer
43
+ * isolation: keep one schema project, and every customer's session resolves its
44
+ * schema from it instead of re-pushing the schema into each customer's org.
45
+ * Requires the `sk_` to carry `ephemeral:mint-any-org`. Omit for the default
46
+ * (the session resolves its schema from its own org). */
47
+ readonly schemaProject?: {
48
+ /** The org that owns the schema project. */
49
+ readonly organizationId: string;
50
+ /** The project the schema was pushed under. */
51
+ readonly projectId: string;
52
+ };
48
53
  readonly syncGroups?: readonly string[];
49
54
  readonly ttlSeconds: number;
50
55
  readonly label?: string;
@@ -108,8 +108,14 @@ export async function mintUserSessionKey(options) {
108
108
  body: JSON.stringify({
109
109
  user: { id: options.userId },
110
110
  ...(options.organizationId ? { organizationId: options.organizationId } : {}),
111
- ...(options.schemaProjectId ? { schemaProjectId: options.schemaProjectId } : {}),
112
- ...(options.schemaOwnerOrgId ? { schemaOwnerOrgId: options.schemaOwnerOrgId } : {}),
111
+ // Flattened to the existing wire keys — the public param is project-centric,
112
+ // the transport contract is unchanged (no coordinated server deploy needed).
113
+ ...(options.schemaProject
114
+ ? {
115
+ schemaProjectId: options.schemaProject.projectId,
116
+ schemaOwnerOrgId: options.schemaProject.organizationId,
117
+ }
118
+ : {}),
113
119
  ...(options.syncGroups ? { syncGroups: options.syncGroups } : {}),
114
120
  ttlSeconds: options.ttlSeconds,
115
121
  ...(options.label ? { label: options.label } : {}),
package/dist/cli.cjs CHANGED
@@ -280564,7 +280564,8 @@ function parseProjectFlag(argv) {
280564
280564
  const eq = argv.find((a) => a.startsWith("--project="));
280565
280565
  return eq ? eq.slice("--project=".length) || void 0 : void 0;
280566
280566
  }
280567
- async function deviceLogin(argv) {
280567
+ async function deviceLogin(argv, deps = {}) {
280568
+ const openUrl = deps.openUrl ?? openBrowser;
280568
280569
  Ie(`${brand("ablo")} login`);
280569
280570
  const requested = parseProjectFlag(argv) ?? getActiveProject()?.slug;
280570
280571
  const targetProject = requested === DEFAULT_PROFILE ? void 0 : requested;
@@ -280599,7 +280600,7 @@ async function deviceLogin(argv) {
280599
280600
  Me(`${import_picocolors7.default.bold(code.user_code)}
280600
280601
 
280601
280602
  ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
280602
- openBrowser(url);
280603
+ openUrl(url);
280603
280604
  const s = Y2();
280604
280605
  s.start("Waiting for approval\u2026");
280605
280606
  let pollMs = (code.interval ?? 5) * 1e3;
@@ -280690,8 +280691,8 @@ ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
280690
280691
  `${import_picocolors7.default.green("\u2713")} Logged in ${import_picocolors7.default.dim("(sandbox)")}${where}. Run ${import_picocolors7.default.bold("npx ablo push")} to push your schema.`
280691
280692
  );
280692
280693
  }
280693
- async function login(argv = []) {
280694
- await deviceLogin(argv);
280694
+ async function login(argv = [], deps = {}) {
280695
+ await deviceLogin(argv, deps);
280695
280696
  }
280696
280697
  function logout() {
280697
280698
  const removed = clearCredential();
@@ -67,6 +67,73 @@ anything extra.
67
67
 
68
68
  ---
69
69
 
70
+ ## Declaring conflict behaviour in the schema (Axis 3)
71
+
72
+ The two enforcement layers above are decided **per write** (claim a row, or pass
73
+ `readAt` + `onStale`). You can also declare a model's **default** conflict
74
+ disposition once, in the schema, so every commit to that model is governed
75
+ without per-call wiring. This is the third coordination axis — orthogonal to
76
+ `policy` (who may read a row) and `groups` (which delta channels it fans into).
77
+
78
+ Add a `conflict` map to `model(...)`, keyed by the **committer's** participant
79
+ kind (`user` / `agent` / `system`), with the same `onStale` vocabulary as values:
80
+
81
+ | value | meaning |
82
+ |---|---|
83
+ | `'overwrite'` | the write wins; that committer is never blocked. |
84
+ | `'reject'` | the write is refused; that committer yields to a held claim / stale snapshot. |
85
+ | `'notify'` | hold the write and hand back the current value so the committer re-reads and re-applies (stale writes only). |
86
+
87
+ ```ts
88
+ import { model, field } from '@abloatai/ablo/schema';
89
+
90
+ export const card = model('card', {
91
+ title: field.string(),
92
+ }, {
93
+ // "a human's edit always wins (never blocked); an agent yields"
94
+ conflict: { user: 'overwrite', agent: 'reject' },
95
+ });
96
+ ```
97
+
98
+ ### Composable helpers
99
+
100
+ For the same map with an authoring surface that reads like the rest of the DSL,
101
+ use the disposition helpers and the `coordination(...)` combinator (a `cn`/`cx`
102
+ for conflict policy — later rules win on key collisions):
103
+
104
+ ```ts
105
+ import { model, field, coordination, humansOverwrite, agentsReject } from '@abloatai/ablo/schema';
106
+
107
+ export const card = model('card', {
108
+ title: field.string(),
109
+ }, {
110
+ conflict: coordination(humansOverwrite(), agentsReject()),
111
+ // → { user: 'overwrite', agent: 'reject' }
112
+ });
113
+ ```
114
+
115
+ Helpers, one per disposition: `humansOverwrite` / `humansReject` / `humansNotify`,
116
+ `agentsOverwrite` / `agentsReject` / `agentsNotify`,
117
+ `systemOverwrite` / `systemReject` / `systemNotify`.
118
+
119
+ ### How it relates to per-write coordination
120
+
121
+ - The `conflict` map is **pure, serializable data**: it ships in your pushed
122
+ schema (`npx ablo push`) and the engine interprets it at the commit
123
+ chokepoint — there is no per-model code on the server.
124
+ - An **omitted committer kind falls through to the engine default**: reject, and
125
+ honor a per-write `onStale: 'notify'`. Declaring `conflict` is purely
126
+ additive — existing schemas behave exactly as before.
127
+ - It sets the **default** disposition; a per-write `onStale` and a held `claim`
128
+ still apply on top as described above. Think of `conflict` as "the house rule
129
+ for this model," and `claim` / `onStale` as what an individual write does
130
+ within it.
131
+
132
+ The `ConflictAxis` type (also available as `Ablo.Conflict.Axis`) and the
133
+ `interpretConflictAxis` interpreter are exported for composing custom policies.
134
+
135
+ ---
136
+
70
137
  ## The claim state object
71
138
 
72
139
  The claim state object is the live record that a participant is coordinating work on
package/docs/identity.md CHANGED
@@ -156,6 +156,40 @@ The identity is a **participant** — and a participant is either a human
156
156
  [Agents are participants too](#agents-are-participants-too) below. Everything in
157
157
  the next two sections applies to both.
158
158
 
159
+ ## Your schema lives in a project; your users commit to it
160
+
161
+ The default is simple: your schema lives in a **project**, you push it once, and
162
+ every session you mint resolves against it. Your end-users **don't have Ablo
163
+ accounts** — your server's `sk_` mints an `ek_` per user, and by default that
164
+ session lands in your project's own org. All your users share one schema, one
165
+ data tenant, isolated from each other by sync-groups. That's the whole story for
166
+ most apps.
167
+
168
+ **Add-on — org-per-customer isolation.** If you need each customer to be its own
169
+ hard tenant (separate row-level isolation, optionally a separate database) you'd
170
+ otherwise have to re-push your schema into every customer's org. Instead, keep one
171
+ project as the home of your schema and point each customer's session's *schema*
172
+ at it while its *data* stays in the customer's org:
173
+
174
+ ```ts
175
+ await mintUserSessionKey({
176
+ apiKey: platformKey, // sk_ with the `ephemeral:mint-any-org` scope
177
+ userId,
178
+ organizationId, // DATA → this customer's org (RLS-isolated tenant)
179
+ schemaProject: { // SCHEMA → the project that owns your schema
180
+ organizationId: schemaOwnerOrgId,
181
+ projectId: schemaProjectId,
182
+ },
183
+ ttlSeconds: 3600,
184
+ });
185
+ ```
186
+
187
+ Server-side, the model **shape** loads from your schema project but column
188
+ enrichment and the tenant connection still target `organizationId` — so the
189
+ shared schema only *describes* the shape; the data plane stays the customer's and
190
+ can't cross-leak. Omit these fields for the default above. Requires a platform
191
+ `sk_` with `ephemeral:mint-any-org`.
192
+
159
193
  ## The two halves of scoping
160
194
 
161
195
  Scoping is two declarations that meet in the middle. One describes the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "description": "The Collaboration Layer For AI Agents",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",