@abloatai/ablo 0.16.0 → 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,44 @@
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
+
26
+ ## 0.16.1
27
+
28
+ ### Patch Changes
29
+
30
+ - **Fix `ablo login` against the standalone auth server.** The device flow now
31
+ targets two origins instead of one: the RFC 8628 device endpoints
32
+ (`/api/auth/device/*`) go to the identity server (`auth.abloatai.com`, override
33
+ `ABLO_AUTH_URL`), while the human approval page (`/cli`), sign-up, and the
34
+ key-handoff route (`/api/cli/provision-key`) go to the dashboard host
35
+ (`www.abloatai.com`, new override `ABLO_DASHBOARD_URL`). Previously every step
36
+ ran against `www`, where the device endpoints no longer resolve —
37
+ producing "Couldn't start login… Is the dashboard reachable?". The CLI now also
38
+ builds the approval URL itself rather than trusting the server's
39
+ `verification_uri`, which (being a relative `/cli`) resolved against the auth
40
+ server's origin to a 404.
41
+
3
42
  ## 0.16.0
4
43
 
5
44
  ### Minor Changes
@@ -9,7 +48,6 @@
9
48
  per committer kind (`user` / `agent` / `system`) — right next to its fields, using the
10
49
  same `overwrite | reject | notify` vocabulary as the `onStale` write guard. It is a
11
50
  third axis, orthogonal to `policy` (read access) and `groups` (delta routing).
12
-
13
51
  - **`conflict` on `model()`** — a plain, serializable disposition map. Pure data, so it
14
52
  round-trips through the schema registry to the server; the generic engine interprets it
15
53
  at the commit chokepoint (no per-model logic in the engine).
@@ -26,7 +64,7 @@
26
64
  ```ts
27
65
  import { coordination, humansOverwrite, agentsReject } from '@abloatai/ablo/schema';
28
66
 
29
- conflict: coordination(humansOverwrite(), agentsReject())
67
+ conflict: coordination(humansOverwrite(), agentsReject());
30
68
  // → { user: 'overwrite', agent: 'reject' }
31
69
  ```
32
70
 
@@ -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
@@ -280541,7 +280541,9 @@ init_cjs_shims();
280541
280541
  var import_child_process = require("child_process");
280542
280542
  var import_picocolors7 = __toESM(require_picocolors(), 1);
280543
280543
  var CLIENT_ID = "ablo-cli";
280544
- var AUTH_URL = (process.env.ABLO_AUTH_URL ?? "https://www.abloatai.com").replace(/\/+$/, "");
280544
+ var stripSlash = (u2) => u2.replace(/\/+$/, "");
280545
+ var AUTH_URL = stripSlash(process.env.ABLO_AUTH_URL ?? "https://auth.abloatai.com");
280546
+ var DASHBOARD_URL = stripSlash(process.env.ABLO_DASHBOARD_URL ?? "https://www.abloatai.com");
280545
280547
  var sleep = (ms) => new Promise((r2) => setTimeout(r2, ms));
280546
280548
  function openBrowser(url) {
280547
280549
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
@@ -280562,7 +280564,8 @@ function parseProjectFlag(argv) {
280562
280564
  const eq = argv.find((a) => a.startsWith("--project="));
280563
280565
  return eq ? eq.slice("--project=".length) || void 0 : void 0;
280564
280566
  }
280565
- async function deviceLogin(argv) {
280567
+ async function deviceLogin(argv, deps = {}) {
280568
+ const openUrl = deps.openUrl ?? openBrowser;
280566
280569
  Ie(`${brand("ablo")} login`);
280567
280570
  const requested = parseProjectFlag(argv) ?? getActiveProject()?.slug;
280568
280571
  const targetProject = requested === DEFAULT_PROFILE ? void 0 : requested;
@@ -280593,11 +280596,11 @@ async function deviceLogin(argv) {
280593
280596
  }
280594
280597
  const code = await codeRes.json();
280595
280598
  const approvePath = `/cli?user_code=${code.user_code}`;
280596
- const url = account === "signup" ? `${AUTH_URL}/signup?next=${encodeURIComponent(approvePath)}` : code.verification_uri_complete ?? code.verification_uri;
280599
+ const url = account === "signup" ? `${DASHBOARD_URL}/signup?next=${encodeURIComponent(approvePath)}` : `${DASHBOARD_URL}${approvePath}`;
280597
280600
  Me(`${import_picocolors7.default.bold(code.user_code)}
280598
280601
 
280599
280602
  ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
280600
- openBrowser(url);
280603
+ openUrl(url);
280601
280604
  const s = Y2();
280602
280605
  s.start("Waiting for approval\u2026");
280603
280606
  let pollMs = (code.interval ?? 5) * 1e3;
@@ -280646,7 +280649,7 @@ ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
280646
280649
  s.message(
280647
280650
  targetProject ? `Provisioning keys for ${targetProject}\u2026` : "Provisioning a sandbox key\u2026"
280648
280651
  );
280649
- const provRes = await fetch(`${AUTH_URL}/api/cli/provision-key`, {
280652
+ const provRes = await fetch(`${DASHBOARD_URL}/api/cli/provision-key`, {
280650
280653
  method: "POST",
280651
280654
  headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json" },
280652
280655
  // Scope the minted keys to the chosen project (`--project`/active), with
@@ -280661,7 +280664,7 @@ ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
280661
280664
  s.stop("Could not provision a key.");
280662
280665
  const reason = provRes ? (await provRes.json().catch(() => ({}))).error : void 0;
280663
280666
  if (reason) M2.error(reason);
280664
- else if (provRes) M2.error(`Key provisioning returned ${provRes.status} from ${AUTH_URL}/api/cli/provision-key.`);
280667
+ else if (provRes) M2.error(`Key provisioning returned ${provRes.status} from ${DASHBOARD_URL}/api/cli/provision-key.`);
280665
280668
  M2.error(
280666
280669
  `The browser approval succeeded but the key handoff failed. Try again, or grab a ${import_picocolors7.default.bold("sk_test_")} key from the dashboard and set ${import_picocolors7.default.bold("ABLO_API_KEY")}.`
280667
280670
  );
@@ -280688,8 +280691,8 @@ ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
280688
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.`
280689
280692
  );
280690
280693
  }
280691
- async function login(argv = []) {
280692
- await deviceLogin(argv);
280694
+ async function login(argv = [], deps = {}) {
280695
+ await deviceLogin(argv, deps);
280693
280696
  }
280694
280697
  function logout() {
280695
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.0",
3
+ "version": "0.16.2",
4
4
  "description": "The Collaboration Layer For AI Agents",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",