@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 +40 -2
- package/dist/auth/index.d.ts +12 -7
- package/dist/auth/index.js +8 -2
- package/dist/cli.cjs +11 -8
- package/docs/coordination.md +67 -0
- package/docs/identity.md +34 -0
- package/package.json +1 -1
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
|
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
readonly
|
|
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;
|
package/dist/auth/index.js
CHANGED
|
@@ -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
|
-
|
|
112
|
-
|
|
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
|
|
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" ? `${
|
|
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
|
-
|
|
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(`${
|
|
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 ${
|
|
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();
|
package/docs/coordination.md
CHANGED
|
@@ -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
|