@abloatai/ablo 0.15.1 → 0.16.1
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 +60 -0
- package/dist/auth/index.d.ts +7 -0
- package/dist/auth/index.js +2 -0
- package/dist/cli.cjs +6 -4
- package/dist/client/Ablo.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +1 -1
- package/dist/policy/types.d.ts +43 -1
- package/dist/policy/types.js +49 -2
- package/dist/schema/coordination.d.ts +57 -0
- package/dist/schema/coordination.js +56 -0
- package/dist/schema/index.d.ts +2 -1
- package/dist/schema/index.js +3 -0
- package/dist/schema/model.d.ts +27 -0
- package/dist/schema/model.js +3 -0
- package/dist/schema/serialize.d.ts +8 -3
- package/dist/schema/serialize.js +8 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- **Fix `ablo login` against the standalone auth server.** The device flow now
|
|
8
|
+
targets two origins instead of one: the RFC 8628 device endpoints
|
|
9
|
+
(`/api/auth/device/*`) go to the identity server (`auth.abloatai.com`, override
|
|
10
|
+
`ABLO_AUTH_URL`), while the human approval page (`/cli`), sign-up, and the
|
|
11
|
+
key-handoff route (`/api/cli/provision-key`) go to the dashboard host
|
|
12
|
+
(`www.abloatai.com`, new override `ABLO_DASHBOARD_URL`). Previously every step
|
|
13
|
+
ran against `www`, where the Better Auth device endpoints no longer resolve —
|
|
14
|
+
producing "Couldn't start login… Is the dashboard reachable?". The CLI now also
|
|
15
|
+
builds the approval URL itself rather than trusting the server's
|
|
16
|
+
`verification_uri`, which (being a relative `/cli`) resolved against the auth
|
|
17
|
+
server's origin to a 404.
|
|
18
|
+
|
|
19
|
+
## 0.16.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- **Axis 3 — declare write-conflict behaviour in the schema (new).** A model can now
|
|
24
|
+
state what happens when a commit collides with a foreign claim or a stale snapshot —
|
|
25
|
+
per committer kind (`user` / `agent` / `system`) — right next to its fields, using the
|
|
26
|
+
same `overwrite | reject | notify` vocabulary as the `onStale` write guard. It is a
|
|
27
|
+
third axis, orthogonal to `policy` (read access) and `groups` (delta routing).
|
|
28
|
+
- **`conflict` on `model()`** — a plain, serializable disposition map. Pure data, so it
|
|
29
|
+
round-trips through the schema registry to the server; the generic engine interprets it
|
|
30
|
+
at the commit chokepoint (no per-model logic in the engine).
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// "a human's edit always wins (never blocked); an agent yields"
|
|
34
|
+
conflict: { user: 'overwrite', agent: 'reject' }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **Composable authoring helpers (new, from `@abloatai/ablo/schema`)** — disposition
|
|
38
|
+
functions plus a `cn`/`cx`-style combinator, so conflict policy reads like the rest of
|
|
39
|
+
the DSL (`relation.belongsTo()`) and like modern config (`plugins: [admin(), …]`):
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { coordination, humansOverwrite, agentsReject } from '@abloatai/ablo/schema';
|
|
43
|
+
|
|
44
|
+
conflict: coordination(humansOverwrite(), agentsReject());
|
|
45
|
+
// → { user: 'overwrite', agent: 'reject' }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Exports: `coordination`, `humansOverwrite` / `humansReject` / `humansNotify`,
|
|
49
|
+
`agentsOverwrite` / `agentsReject` / `agentsNotify`,
|
|
50
|
+
`systemOverwrite` / `systemReject` / `systemNotify`, and the `ConflictRule` type.
|
|
51
|
+
|
|
52
|
+
- An omitted committer kind falls through to the engine default (reject; honor
|
|
53
|
+
`onStale: 'notify'`), so this is fully additive — existing schemas are unchanged.
|
|
54
|
+
New public types `ConflictAxis` (also `Ablo.Conflict.Axis`) and the
|
|
55
|
+
`interpretConflictAxis` interpreter are exported for custom policy composition.
|
|
56
|
+
|
|
57
|
+
- **First-party shared schema for ephemeral keys (new).** `mintUserSessionKey` now accepts
|
|
58
|
+
`schemaProjectId` + `schemaOwnerOrgId`, binding the minted `ek_` to a schema owner-org +
|
|
59
|
+
project so **schema** resolves org-independently (one schema serves all of an integrator's
|
|
60
|
+
end-user orgs) while **data** stays scoped to `organizationId`. Requires the `sk_` to carry
|
|
61
|
+
`ephemeral:mint-any-org`; omit both for the existing per-org (BYO) behaviour.
|
|
62
|
+
|
|
3
63
|
## 0.15.1
|
|
4
64
|
|
|
5
65
|
### Patch Changes
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -38,6 +38,13 @@ 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
48
|
readonly syncGroups?: readonly string[];
|
|
42
49
|
readonly ttlSeconds: number;
|
|
43
50
|
readonly label?: string;
|
package/dist/auth/index.js
CHANGED
|
@@ -108,6 +108,8 @@ 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
113
|
...(options.syncGroups ? { syncGroups: options.syncGroups } : {}),
|
|
112
114
|
ttlSeconds: options.ttlSeconds,
|
|
113
115
|
...(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";
|
|
@@ -280593,7 +280595,7 @@ async function deviceLogin(argv) {
|
|
|
280593
280595
|
}
|
|
280594
280596
|
const code = await codeRes.json();
|
|
280595
280597
|
const approvePath = `/cli?user_code=${code.user_code}`;
|
|
280596
|
-
const url = account === "signup" ? `${
|
|
280598
|
+
const url = account === "signup" ? `${DASHBOARD_URL}/signup?next=${encodeURIComponent(approvePath)}` : `${DASHBOARD_URL}${approvePath}`;
|
|
280597
280599
|
Me(`${import_picocolors7.default.bold(code.user_code)}
|
|
280598
280600
|
|
|
280599
280601
|
${import_picocolors7.default.dim(url)}`, "Approve in your browser");
|
|
@@ -280646,7 +280648,7 @@ ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
|
|
|
280646
280648
|
s.message(
|
|
280647
280649
|
targetProject ? `Provisioning keys for ${targetProject}\u2026` : "Provisioning a sandbox key\u2026"
|
|
280648
280650
|
);
|
|
280649
|
-
const provRes = await fetch(`${
|
|
280651
|
+
const provRes = await fetch(`${DASHBOARD_URL}/api/cli/provision-key`, {
|
|
280650
280652
|
method: "POST",
|
|
280651
280653
|
headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json" },
|
|
280652
280654
|
// Scope the minted keys to the chosen project (`--project`/active), with
|
|
@@ -280661,7 +280663,7 @@ ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
|
|
|
280661
280663
|
s.stop("Could not provision a key.");
|
|
280662
280664
|
const reason = provRes ? (await provRes.json().catch(() => ({}))).error : void 0;
|
|
280663
280665
|
if (reason) M2.error(reason);
|
|
280664
|
-
else if (provRes) M2.error(`Key provisioning returned ${provRes.status} from ${
|
|
280666
|
+
else if (provRes) M2.error(`Key provisioning returned ${provRes.status} from ${DASHBOARD_URL}/api/cli/provision-key.`);
|
|
280665
280667
|
M2.error(
|
|
280666
280668
|
`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
280669
|
);
|
package/dist/client/Ablo.d.ts
CHANGED
|
@@ -1010,6 +1010,7 @@ export declare namespace Ablo {
|
|
|
1010
1010
|
type Operation = _Policy.ConflictOperation;
|
|
1011
1011
|
type Decision = _Policy.ConflictDecision;
|
|
1012
1012
|
type Policy = _Policy.ConflictPolicy;
|
|
1013
|
+
type Axis = _Policy.ConflictAxis;
|
|
1013
1014
|
}
|
|
1014
1015
|
namespace Commit {
|
|
1015
1016
|
type Wait = import('./Ablo.js').CommitWait;
|
package/dist/index.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ import { Ablo } from './client/Ablo.js';
|
|
|
58
58
|
export default Ablo;
|
|
59
59
|
export { dataSource, abloSource, sourceEventForOperation, signAbloSourceRequest, verifyAbloSourceRequest, } from './source/index.js';
|
|
60
60
|
export { createSourceConnector, type SourceConnector, type SourceConnectorOptions, type ConnectorStatus, } from './source/connector.js';
|
|
61
|
-
export { defaultPolicy, capabilityPreemptPolicy } from './policy/index.js';
|
|
61
|
+
export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from './policy/index.js';
|
|
62
62
|
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloNotFoundError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, classifyRecovery, recoveryClassSchema, RECOVERY_CLASSES, } from './errors.js';
|
|
63
63
|
export type { CommitReceipt, RequiredCapability } from './errors.js';
|
|
64
64
|
export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec, RecoveryClass } from './errors.js';
|
package/dist/index.js
CHANGED
|
@@ -80,7 +80,7 @@ export { createSourceConnector, } from './source/connector.js';
|
|
|
80
80
|
// (reject-on-stale) is already applied server-side, so you only import it
|
|
81
81
|
// to COMPOSE a custom policy. Leave it alone and stale writes are rejected
|
|
82
82
|
// safely by default. Type counterparts live under `Ablo.Conflict.*`.
|
|
83
|
-
export { defaultPolicy, capabilityPreemptPolicy } from './policy/index.js';
|
|
83
|
+
export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from './policy/index.js';
|
|
84
84
|
// Typed error hierarchy — Stripe-style. One import gets every class
|
|
85
85
|
// consumers need to discriminate failures (`e instanceof AbloX` or
|
|
86
86
|
// `e.type === 'AbloX'`) plus the HTTP-response translator.
|
package/dist/policy/index.d.ts
CHANGED
|
@@ -15,5 +15,5 @@
|
|
|
15
15
|
* };
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
|
-
export type { Conflict, ConflictDecision, ConflictKind, ConflictOperation, ConflictPolicy, StaleContextConflict, ClaimHeldConflict, } from './types.js';
|
|
19
|
-
export { defaultPolicy, capabilityPreemptPolicy } from './types.js';
|
|
18
|
+
export type { Conflict, ConflictAxis, ConflictDecision, ConflictKind, ConflictOperation, ConflictPolicy, StaleContextConflict, ClaimHeldConflict, } from './types.js';
|
|
19
|
+
export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from './types.js';
|
package/dist/policy/index.js
CHANGED
package/dist/policy/types.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Adding new shapes is additive on the discriminated union.
|
|
8
8
|
*/
|
|
9
9
|
import type { ParticipantRef } from '../types/streams.js';
|
|
10
|
+
import type { OnStaleMode } from '../coordination/schema.js';
|
|
10
11
|
export type ConflictKind = 'stale_context' | 'claim_held';
|
|
11
12
|
/** Fields shared by every conflict shape. */
|
|
12
13
|
interface ConflictBase {
|
|
@@ -134,7 +135,7 @@ export type ConflictPolicy = (conflict: Conflict) => ConflictDecision | Promise<
|
|
|
134
135
|
* detection. This preserves the legacy always-reject default for callers that
|
|
135
136
|
* don't opt into `notify`.
|
|
136
137
|
*/
|
|
137
|
-
export declare const defaultPolicy:
|
|
138
|
+
export declare const defaultPolicy: (conflict: Conflict) => ConflictDecision;
|
|
138
139
|
/**
|
|
139
140
|
* Capability-gated preemption. An `claim_held` conflict is PREEMPTED when the
|
|
140
141
|
* committer holds the `claim.preempt` operation in its capability allowlist
|
|
@@ -144,4 +145,45 @@ export declare const defaultPolicy: ConflictPolicy;
|
|
|
144
145
|
* policy. The authorization is the capability, not an identity string.
|
|
145
146
|
*/
|
|
146
147
|
export declare const capabilityPreemptPolicy: ConflictPolicy;
|
|
148
|
+
/**
|
|
149
|
+
* **Axis 3 — declared write-conflict disposition, per committer kind.**
|
|
150
|
+
*
|
|
151
|
+
* A model declares this in its schema (`conflict: { user: 'overwrite', agent:
|
|
152
|
+
* 'reject' }`); the generic engine interprets it at the commit chokepoint. It's
|
|
153
|
+
* pure data — the same `OnStaleMode` vocabulary used by write guards
|
|
154
|
+
* (`'reject' | 'overwrite' | 'notify'`) — so it serializes through the schema
|
|
155
|
+
* registry to the schema-agnostic server, which names no app model.
|
|
156
|
+
*
|
|
157
|
+
* Keys are the COMMITTER's participant kind (server-derived, forge-proof): an
|
|
158
|
+
* omitted kind falls through to the engine default. So `{ user: 'overwrite',
|
|
159
|
+
* agent: 'reject' }` reads "a human's write wins (never blocked); an agent's
|
|
160
|
+
* write yields", and `system` (unlisted) takes the default.
|
|
161
|
+
*/
|
|
162
|
+
export interface ConflictAxis {
|
|
163
|
+
/** What happens when a human (`user` session) commits into a conflict. */
|
|
164
|
+
readonly user?: OnStaleMode;
|
|
165
|
+
/** What happens when an AI `agent` commits into a conflict. */
|
|
166
|
+
readonly agent?: OnStaleMode;
|
|
167
|
+
/** What happens when a `system` / automation actor commits into a conflict. */
|
|
168
|
+
readonly system?: OnStaleMode;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Interpret a declared {@link ConflictAxis} for a concrete conflict — pure and
|
|
172
|
+
* synchronous (no I/O), so it runs on either side of the schema-agnostic
|
|
173
|
+
* boundary. Looks up the committer's kind and maps the declared `OnStaleMode`:
|
|
174
|
+
*
|
|
175
|
+
* - undefined → the engine default (`defaultPolicy`: reject; honor
|
|
176
|
+
* `onStale: 'notify'` on a stale write)
|
|
177
|
+
* - `overwrite` → `allow` — the write wins / the committer is never blocked
|
|
178
|
+
* - `reject` → `reject` — the committer yields
|
|
179
|
+
* - `notify` → on `stale_context`: notify + hold (re-read & re-apply);
|
|
180
|
+
* on `claim_held`: notify has no held op to reconcile against
|
|
181
|
+
* (see {@link ConflictDecision} `notify`), so degrade to
|
|
182
|
+
* `reject` rather than silently allowing a claimed-row write.
|
|
183
|
+
*
|
|
184
|
+
* NOTE: this is a generic interpretation only. Server-side invariants (e.g. an
|
|
185
|
+
* agent may never bypass a foreign claim) are enforced where the decision is
|
|
186
|
+
* applied, not here.
|
|
187
|
+
*/
|
|
188
|
+
export declare function interpretConflictAxis(axis: ConflictAxis, conflict: Conflict): ConflictDecision;
|
|
147
189
|
export {};
|
package/dist/policy/types.js
CHANGED
|
@@ -20,14 +20,19 @@
|
|
|
20
20
|
* detection. This preserves the legacy always-reject default for callers that
|
|
21
21
|
* don't opt into `notify`.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
// Typed by its real SYNCHRONOUS shape (via `satisfies`) rather than the
|
|
24
|
+
// async-permissive `ConflictPolicy` alias, so sync callers like
|
|
25
|
+
// `interpretConflictAxis` and `capabilityPreemptPolicy` get a plain
|
|
26
|
+
// `ConflictDecision` back (not `… | Promise<…>`). Still assignable to
|
|
27
|
+
// `ConflictPolicy` everywhere it's used as a policy.
|
|
28
|
+
export const defaultPolicy = ((conflict) => {
|
|
24
29
|
if (conflict.kind !== 'stale_context') {
|
|
25
30
|
return { action: 'reject', reason: 'claim_conflict' };
|
|
26
31
|
}
|
|
27
32
|
return conflict.requestedMode === 'notify'
|
|
28
33
|
? { action: 'notify', reason: 'stale_notify_hold' }
|
|
29
34
|
: { action: 'reject', reason: 'stale_context' };
|
|
30
|
-
};
|
|
35
|
+
});
|
|
31
36
|
/**
|
|
32
37
|
* Capability-gated preemption. An `claim_held` conflict is PREEMPTED when the
|
|
33
38
|
* committer holds the `claim.preempt` operation in its capability allowlist
|
|
@@ -43,3 +48,45 @@ export const capabilityPreemptPolicy = (conflict) => {
|
|
|
43
48
|
}
|
|
44
49
|
return defaultPolicy(conflict);
|
|
45
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* Interpret a declared {@link ConflictAxis} for a concrete conflict — pure and
|
|
53
|
+
* synchronous (no I/O), so it runs on either side of the schema-agnostic
|
|
54
|
+
* boundary. Looks up the committer's kind and maps the declared `OnStaleMode`:
|
|
55
|
+
*
|
|
56
|
+
* - undefined → the engine default (`defaultPolicy`: reject; honor
|
|
57
|
+
* `onStale: 'notify'` on a stale write)
|
|
58
|
+
* - `overwrite` → `allow` — the write wins / the committer is never blocked
|
|
59
|
+
* - `reject` → `reject` — the committer yields
|
|
60
|
+
* - `notify` → on `stale_context`: notify + hold (re-read & re-apply);
|
|
61
|
+
* on `claim_held`: notify has no held op to reconcile against
|
|
62
|
+
* (see {@link ConflictDecision} `notify`), so degrade to
|
|
63
|
+
* `reject` rather than silently allowing a claimed-row write.
|
|
64
|
+
*
|
|
65
|
+
* NOTE: this is a generic interpretation only. Server-side invariants (e.g. an
|
|
66
|
+
* agent may never bypass a foreign claim) are enforced where the decision is
|
|
67
|
+
* applied, not here.
|
|
68
|
+
*/
|
|
69
|
+
export function interpretConflictAxis(axis, conflict) {
|
|
70
|
+
const mode = axis[conflict.committer.kind];
|
|
71
|
+
if (mode === undefined)
|
|
72
|
+
return defaultPolicy(conflict);
|
|
73
|
+
switch (mode) {
|
|
74
|
+
case 'overwrite':
|
|
75
|
+
return { action: 'allow', note: 'conflict:overwrite' };
|
|
76
|
+
case 'reject':
|
|
77
|
+
return {
|
|
78
|
+
action: 'reject',
|
|
79
|
+
reason: conflict.kind === 'claim_held' ? 'claim_conflict' : 'stale_context',
|
|
80
|
+
};
|
|
81
|
+
case 'notify':
|
|
82
|
+
return conflict.kind === 'stale_context'
|
|
83
|
+
? { action: 'notify', reason: 'stale_notify_hold' }
|
|
84
|
+
: { action: 'reject', reason: 'claim_conflict' };
|
|
85
|
+
default: {
|
|
86
|
+
// Exhaustiveness backstop: a future `OnStaleMode` member surfaces as a
|
|
87
|
+
// localized compile error here, not a missing-return at the signature.
|
|
88
|
+
const _exhaustive = mode;
|
|
89
|
+
return _exhaustive;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordination authoring helpers for the model `conflict` axis.
|
|
3
|
+
*
|
|
4
|
+
* Composable disposition functions + a `cn`/`cx`-style combinator, so a model
|
|
5
|
+
* declares conflict behaviour the way the rest of the DSL reads
|
|
6
|
+
* (`relation.belongsTo()`, `field.string()`) — and the way modern libraries
|
|
7
|
+
* compose config (Better Auth's `plugins: [admin(), twoFactor()]`, shadcn's
|
|
8
|
+
* `cx(a, b)`) — instead of a raw disposition map:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { coordination, humansOverwrite, agentsReject } from '@abloatai/ablo/schema';
|
|
12
|
+
*
|
|
13
|
+
* conflict: coordination(humansOverwrite(), agentsReject())
|
|
14
|
+
* // → { user: 'overwrite', agent: 'reject' } (a human's write wins, an agent's yields)
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Each helper is named for the exact disposition it applies — the same
|
|
18
|
+
* `overwrite | reject | notify` vocabulary used by write guards (`onStale`) —
|
|
19
|
+
* and returns a partial {@link ConflictAxis}. {@link coordination} merges them
|
|
20
|
+
* (later rules win on key collisions). The result is plain, serializable data —
|
|
21
|
+
* the engine interpreter and schema round-trip are unchanged; this is only a
|
|
22
|
+
* nicer authoring surface.
|
|
23
|
+
*/
|
|
24
|
+
import type { ConflictAxis } from '../policy/types.js';
|
|
25
|
+
/**
|
|
26
|
+
* One coordination rule: a partial {@link ConflictAxis} produced by a
|
|
27
|
+
* disposition helper below. Compose with {@link coordination}.
|
|
28
|
+
*/
|
|
29
|
+
export type ConflictRule = ConflictAxis;
|
|
30
|
+
/** A human's conflicting write OVERWRITES — wins; never blocked (LWW among humans). */
|
|
31
|
+
export declare const humansOverwrite: () => ConflictRule;
|
|
32
|
+
/** A human's conflicting write is REJECTED — yields to a held claim / stale snapshot. */
|
|
33
|
+
export declare const humansReject: () => ConflictRule;
|
|
34
|
+
/** A human's stale write NOTIFIES — re-reads & re-applies instead of clobbering. */
|
|
35
|
+
export declare const humansNotify: () => ConflictRule;
|
|
36
|
+
/** An agent's conflicting write OVERWRITES — wins (rarely wanted). */
|
|
37
|
+
export declare const agentsOverwrite: () => ConflictRule;
|
|
38
|
+
/** An agent's conflicting write is REJECTED — yields to a held claim / stale snapshot. */
|
|
39
|
+
export declare const agentsReject: () => ConflictRule;
|
|
40
|
+
/** An agent's stale write NOTIFIES — re-reads & re-applies instead of clobbering. */
|
|
41
|
+
export declare const agentsNotify: () => ConflictRule;
|
|
42
|
+
/** A system/automation conflicting write OVERWRITES. */
|
|
43
|
+
export declare const systemOverwrite: () => ConflictRule;
|
|
44
|
+
/** A system/automation conflicting write is REJECTED. */
|
|
45
|
+
export declare const systemReject: () => ConflictRule;
|
|
46
|
+
/** A system/automation stale write NOTIFIES (re-read & re-apply). */
|
|
47
|
+
export declare const systemNotify: () => ConflictRule;
|
|
48
|
+
/**
|
|
49
|
+
* Merge coordination rules into one {@link ConflictAxis} — the `cn`/`cx` of
|
|
50
|
+
* conflict policy. Later rules win on key collisions; an omitted committer kind
|
|
51
|
+
* falls through to the engine default at commit time.
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* coordination(humansOverwrite(), agentsReject()) // → { user: 'overwrite', agent: 'reject' }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function coordination(...rules: readonly ConflictRule[]): ConflictAxis;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordination authoring helpers for the model `conflict` axis.
|
|
3
|
+
*
|
|
4
|
+
* Composable disposition functions + a `cn`/`cx`-style combinator, so a model
|
|
5
|
+
* declares conflict behaviour the way the rest of the DSL reads
|
|
6
|
+
* (`relation.belongsTo()`, `field.string()`) — and the way modern libraries
|
|
7
|
+
* compose config (Better Auth's `plugins: [admin(), twoFactor()]`, shadcn's
|
|
8
|
+
* `cx(a, b)`) — instead of a raw disposition map:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { coordination, humansOverwrite, agentsReject } from '@abloatai/ablo/schema';
|
|
12
|
+
*
|
|
13
|
+
* conflict: coordination(humansOverwrite(), agentsReject())
|
|
14
|
+
* // → { user: 'overwrite', agent: 'reject' } (a human's write wins, an agent's yields)
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Each helper is named for the exact disposition it applies — the same
|
|
18
|
+
* `overwrite | reject | notify` vocabulary used by write guards (`onStale`) —
|
|
19
|
+
* and returns a partial {@link ConflictAxis}. {@link coordination} merges them
|
|
20
|
+
* (later rules win on key collisions). The result is plain, serializable data —
|
|
21
|
+
* the engine interpreter and schema round-trip are unchanged; this is only a
|
|
22
|
+
* nicer authoring surface.
|
|
23
|
+
*/
|
|
24
|
+
// ── Humans (user sessions) ──────────────────────────────────────────────
|
|
25
|
+
/** A human's conflicting write OVERWRITES — wins; never blocked (LWW among humans). */
|
|
26
|
+
export const humansOverwrite = () => ({ user: 'overwrite' });
|
|
27
|
+
/** A human's conflicting write is REJECTED — yields to a held claim / stale snapshot. */
|
|
28
|
+
export const humansReject = () => ({ user: 'reject' });
|
|
29
|
+
/** A human's stale write NOTIFIES — re-reads & re-applies instead of clobbering. */
|
|
30
|
+
export const humansNotify = () => ({ user: 'notify' });
|
|
31
|
+
// ── Agents (AI) ─────────────────────────────────────────────────────────
|
|
32
|
+
/** An agent's conflicting write OVERWRITES — wins (rarely wanted). */
|
|
33
|
+
export const agentsOverwrite = () => ({ agent: 'overwrite' });
|
|
34
|
+
/** An agent's conflicting write is REJECTED — yields to a held claim / stale snapshot. */
|
|
35
|
+
export const agentsReject = () => ({ agent: 'reject' });
|
|
36
|
+
/** An agent's stale write NOTIFIES — re-reads & re-applies instead of clobbering. */
|
|
37
|
+
export const agentsNotify = () => ({ agent: 'notify' });
|
|
38
|
+
// ── System / automation ─────────────────────────────────────────────────
|
|
39
|
+
/** A system/automation conflicting write OVERWRITES. */
|
|
40
|
+
export const systemOverwrite = () => ({ system: 'overwrite' });
|
|
41
|
+
/** A system/automation conflicting write is REJECTED. */
|
|
42
|
+
export const systemReject = () => ({ system: 'reject' });
|
|
43
|
+
/** A system/automation stale write NOTIFIES (re-read & re-apply). */
|
|
44
|
+
export const systemNotify = () => ({ system: 'notify' });
|
|
45
|
+
/**
|
|
46
|
+
* Merge coordination rules into one {@link ConflictAxis} — the `cn`/`cx` of
|
|
47
|
+
* conflict policy. Later rules win on key collisions; an omitted committer kind
|
|
48
|
+
* falls through to the engine default at commit time.
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* coordination(humansOverwrite(), agentsReject()) // → { user: 'overwrite', agent: 'reject' }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function coordination(...rules) {
|
|
55
|
+
return Object.assign({}, ...rules);
|
|
56
|
+
}
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -27,7 +27,8 @@ export { tenancySchema, scopedViaRefSchema, policyInputSchema, resolvePolicy, re
|
|
|
27
27
|
export { planeSchema, DEFAULT_PLANE, type SchemaPlane } from './plane.js';
|
|
28
28
|
export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syncDeltaRowSchema, participantKindSchema, confirmationStateSchema, backfillProvenanceSchema, DELTA_PLANES, type SyncDeltaCore, type DeltaAttribution, type DeltaProvenance, type SyncDeltaRow, type ParticipantKind, type ConfirmationState, type BackfillProvenance, } from './sync-delta-row.js';
|
|
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
|
-
export { model, scopeKindOf, type ModelDef, type ModelOptions, type LoadStrategy, type PersistOptions, type RelationRecord, type GrantsRef, } from './model.js';
|
|
30
|
+
export { model, scopeKindOf, type ModelDef, type ModelOptions, type LoadStrategy, type PersistOptions, type RelationRecord, type GrantsRef, type ConflictAxis, } from './model.js';
|
|
31
|
+
export { coordination, humansOverwrite, humansReject, humansNotify, agentsOverwrite, agentsReject, agentsNotify, systemOverwrite, systemReject, systemNotify, type ConflictRule, } from './coordination.js';
|
|
31
32
|
export { mutable, readOnly, type SugarOptions } from './sugar.js';
|
|
32
33
|
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, groupsInputSchema, type GroupsInput, } from './schema.js';
|
|
33
34
|
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, type SchemaJSON, type ModelJSON, type RelationJSON, } from './serialize.js';
|
package/dist/schema/index.js
CHANGED
|
@@ -41,6 +41,9 @@ export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syn
|
|
|
41
41
|
export { syncDeltaActionSchema, wireDeltaDataSchema, participantRefSchema, syncDeltaWireCoreSchema, clientSyncDeltaSchema, serverSyncDeltaSchema, } from './sync-delta-wire.js';
|
|
42
42
|
// Model builder
|
|
43
43
|
export { model, scopeKindOf, } from './model.js';
|
|
44
|
+
// Axis 3 — coordination authoring helpers for the `conflict` axis (composable
|
|
45
|
+
// disposition fns + a `cn`-style combinator).
|
|
46
|
+
export { coordination, humansOverwrite, humansReject, humansNotify, agentsOverwrite, agentsReject, agentsNotify, systemOverwrite, systemReject, systemNotify, } from './coordination.js';
|
|
44
47
|
// Claim-first shorthand: `mutable.lazy({...})` and friends. Read the
|
|
45
48
|
// safety posture and load shape off the verb tokens; everything else
|
|
46
49
|
// falls back to sensible defaults. See sugar.ts for the full pattern.
|
package/dist/schema/model.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ import { type FieldMeta } from './field.js';
|
|
|
23
23
|
import { type Tenancy, type PolicyInput } from './tenancy.js';
|
|
24
24
|
export type { ScopedViaRef, Tenancy, PolicyInput } from './tenancy.js';
|
|
25
25
|
import { type SchemaPlane } from './plane.js';
|
|
26
|
+
import type { ConflictAxis } from '../policy/types.js';
|
|
27
|
+
export type { ConflictAxis } from '../policy/types.js';
|
|
26
28
|
/**
|
|
27
29
|
* Controls when model data is loaded from the server.
|
|
28
30
|
*
|
|
@@ -147,6 +149,28 @@ export interface ModelOptions {
|
|
|
147
149
|
* ```
|
|
148
150
|
*/
|
|
149
151
|
groups?: GroupsInput;
|
|
152
|
+
/**
|
|
153
|
+
* **Axis 3 — write-conflict disposition, per committer kind.** Decides what
|
|
154
|
+
* happens when a commit collides with a foreign claim or a stale snapshot on
|
|
155
|
+
* this model — orthogonal to {@link policy} (read access) and {@link groups}
|
|
156
|
+
* (delta routing). A plain map keyed by the COMMITTER's participant kind
|
|
157
|
+
* (`user` / `agent` / `system`), with the `onStale` vocabulary as values:
|
|
158
|
+
*
|
|
159
|
+
* - `'overwrite'` — the write wins; that committer is never blocked.
|
|
160
|
+
* - `'reject'` — the write is refused; that committer yields.
|
|
161
|
+
* - `'notify'` — hold the write and hand back the current value so the
|
|
162
|
+
* committer re-reads and re-applies (stale writes only).
|
|
163
|
+
*
|
|
164
|
+
* An omitted kind falls through to the engine default (reject; honor
|
|
165
|
+
* `onStale: 'notify'`). It is pure data (serializes through the schema
|
|
166
|
+
* registry to the server) and the generic engine interprets it — so e.g.
|
|
167
|
+
*
|
|
168
|
+
* ```ts
|
|
169
|
+
* // "a human's edit always wins (never blocked); an agent yields"
|
|
170
|
+
* conflict: { user: 'overwrite', agent: 'reject' }
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
conflict?: ConflictAxis;
|
|
150
174
|
/**
|
|
151
175
|
* Whether clients may issue CREATE/UPDATE/DELETE mutations for this
|
|
152
176
|
* model via the `commit` wire protocol. Default: **true** — declaring a
|
|
@@ -301,6 +325,9 @@ export interface ModelDef<Shape extends z.ZodRawShape = z.ZodRawShape, R extends
|
|
|
301
325
|
readonly grants?: GrantsRef;
|
|
302
326
|
/** Explicit non-relational record→group roles (normalized to an array). See {@link ModelOptions.entityRoles}. */
|
|
303
327
|
readonly entityRoles?: readonly EntityRole[];
|
|
328
|
+
/** Axis 3 — declared write-conflict disposition per committer kind. Already
|
|
329
|
+
* canonical pure data (unlike `policy`, no resolve step). See {@link ModelOptions.conflict}. */
|
|
330
|
+
readonly conflict?: ConflictAxis;
|
|
304
331
|
/** Whether wire-level CREATE/UPDATE/DELETE is allowed. See {@link ModelOptions.mutable}. */
|
|
305
332
|
readonly mutable?: boolean;
|
|
306
333
|
/** Defer MobX setup until first observer access. See {@link ModelOptions.lazyObservable}. */
|
package/dist/schema/model.js
CHANGED
|
@@ -99,6 +99,9 @@ export function model(shape, relations, options) {
|
|
|
99
99
|
scope: options?.groups?.root,
|
|
100
100
|
grants: options?.groups?.grants,
|
|
101
101
|
entityRoles: normalizeEntityRoles(options?.groups?.roles),
|
|
102
|
+
// Axis 3 — already canonical pure data (a per-kind disposition map), so it
|
|
103
|
+
// passes through verbatim; no resolve step like `resolvePolicy`.
|
|
104
|
+
conflict: options?.conflict,
|
|
102
105
|
mutable: options?.mutable ?? true,
|
|
103
106
|
lazyObservable: options?.lazyObservable,
|
|
104
107
|
computed: options?.computed,
|
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
* What round-trips:
|
|
15
15
|
* - all model routing/scoping metadata (typename, tableName, load,
|
|
16
16
|
* mutable, the canonical `tenancy` descriptor, bootstrap hints, scope,
|
|
17
|
-
* grants, entityRoles, persist, autoFill,
|
|
17
|
+
* grants, entityRoles, the `conflict` disposition map, persist, autoFill,
|
|
18
|
+
* requiredFields, lazyObservable).
|
|
18
19
|
* NOTE: the authoring sugar (`policy`/`groups`) is normalized away at
|
|
19
|
-
* `model()`-build; only the canonical wire fields cross here.
|
|
20
|
+
* `model()`-build; only the canonical wire fields cross here. `conflict`
|
|
21
|
+
* is already canonical pure data, so it crosses verbatim.
|
|
20
22
|
* - relations (incl. resolved `foreignKeyColumn`)
|
|
21
23
|
* - field metadata (names + type tags), from which validators are rebuilt
|
|
22
24
|
* - identity roles (already pure data)
|
|
@@ -29,7 +31,7 @@
|
|
|
29
31
|
import type { FieldMeta } from './field.js';
|
|
30
32
|
import type { Tenancy } from './tenancy.js';
|
|
31
33
|
import type { SchemaPlane } from './plane.js';
|
|
32
|
-
import type { GrantsRef, LoadStrategy, PersistOptions, AutoFillRule } from './model.js';
|
|
34
|
+
import type { GrantsRef, LoadStrategy, PersistOptions, AutoFillRule, ConflictAxis } from './model.js';
|
|
33
35
|
import type { RelationType } from './relation.js';
|
|
34
36
|
import { type Schema, type SchemaRecord, type IdentityRole, type EntityRole } from './schema.js';
|
|
35
37
|
/** Current schema-JSON envelope version. Bump on a breaking change to the
|
|
@@ -60,6 +62,9 @@ export interface ModelJSON {
|
|
|
60
62
|
readonly scope?: boolean | string;
|
|
61
63
|
readonly grants?: GrantsRef;
|
|
62
64
|
readonly entityRoles?: readonly EntityRole[];
|
|
65
|
+
/** Axis 3 — declared write-conflict disposition per committer kind. Pure data;
|
|
66
|
+
* absent in pre-conflict-axis artifacts → undefined → engine default. */
|
|
67
|
+
readonly conflict?: ConflictAxis;
|
|
63
68
|
readonly bootstrapLimit?: number;
|
|
64
69
|
readonly bootstrapOrderBy?: string;
|
|
65
70
|
readonly mutable?: boolean;
|
package/dist/schema/serialize.js
CHANGED
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
* What round-trips:
|
|
15
15
|
* - all model routing/scoping metadata (typename, tableName, load,
|
|
16
16
|
* mutable, the canonical `tenancy` descriptor, bootstrap hints, scope,
|
|
17
|
-
* grants, entityRoles, persist, autoFill,
|
|
17
|
+
* grants, entityRoles, the `conflict` disposition map, persist, autoFill,
|
|
18
|
+
* requiredFields, lazyObservable).
|
|
18
19
|
* NOTE: the authoring sugar (`policy`/`groups`) is normalized away at
|
|
19
|
-
* `model()`-build; only the canonical wire fields cross here.
|
|
20
|
+
* `model()`-build; only the canonical wire fields cross here. `conflict`
|
|
21
|
+
* is already canonical pure data, so it crosses verbatim.
|
|
20
22
|
* - relations (incl. resolved `foreignKeyColumn`)
|
|
21
23
|
* - field metadata (names + type tags), from which validators are rebuilt
|
|
22
24
|
* - identity roles (already pure data)
|
|
@@ -64,6 +66,7 @@ function modelToJSON(def) {
|
|
|
64
66
|
scope: def.scope,
|
|
65
67
|
grants: def.grants,
|
|
66
68
|
entityRoles: def.entityRoles,
|
|
69
|
+
conflict: def.conflict,
|
|
67
70
|
bootstrapLimit: def.bootstrapLimit,
|
|
68
71
|
bootstrapOrderBy: def.bootstrapOrderBy,
|
|
69
72
|
mutable: def.mutable,
|
|
@@ -171,6 +174,9 @@ function modelFromJSON(json) {
|
|
|
171
174
|
scope: json.scope,
|
|
172
175
|
grants: json.grants,
|
|
173
176
|
entityRoles: json.entityRoles,
|
|
177
|
+
// Axis 3 — pure data; absent in pre-conflict-axis artifacts → undefined →
|
|
178
|
+
// the commit path falls through to the function registry / engine default.
|
|
179
|
+
conflict: json.conflict,
|
|
174
180
|
mutable: json.mutable,
|
|
175
181
|
lazyObservable: json.lazyObservable,
|
|
176
182
|
// computed getters are closures and intentionally not serialized; a
|