@abloatai/ablo 0.12.0 → 0.13.0
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/AGENTS.md +2 -2
- package/CHANGELOG.md +19 -0
- package/README.md +3 -3
- package/dist/cli.cjs +149 -40
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +2 -2
- package/dist/schema/model.d.ts +38 -84
- package/dist/schema/model.js +12 -12
- package/dist/schema/roles.d.ts +49 -0
- package/dist/schema/roles.js +21 -0
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.js +1 -1
- package/dist/schema/serialize.d.ts +4 -2
- package/dist/schema/serialize.js +4 -2
- package/dist/schema/sugar.d.ts +7 -28
- package/dist/schema/sugar.js +2 -7
- package/dist/schema/sync-delta-row.d.ts +2 -0
- package/dist/schema/sync-delta-row.js +2 -1
- package/dist/schema/tenancy.d.ts +67 -28
- package/dist/schema/tenancy.js +93 -23
- package/dist/server/commit.d.ts +8 -3
- package/docs/api.md +1 -1
- package/docs/cli.md +43 -4
- package/docs/client-behavior.md +2 -2
- package/docs/coordination.md +1 -1
- package/docs/examples/agent-human.md +6 -6
- package/docs/examples/ai-sdk-tool.md +1 -1
- package/docs/examples/existing-python-backend.md +0 -2
- package/docs/examples/nextjs.md +2 -2
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/examples/server-agent.md +4 -4
- package/docs/identity.md +27 -20
- package/docs/index.md +0 -1
- package/docs/integration-guide.md +12 -9
- package/docs/interaction-model.md +1 -1
- package/docs/mcp.md +17 -5
- package/docs/quickstart.md +3 -3
- package/llms.txt +2 -3
- package/package.json +3 -2
- package/docs/mcp/claude-code.md +0 -35
- package/docs/mcp/cursor.md +0 -35
- package/docs/mcp/windsurf.md +0 -33
- package/docs/roadmap.md +0 -55
- package/docs/the-loop.md +0 -21
- package/llms-full.txt +0 -396
package/AGENTS.md
CHANGED
|
@@ -31,7 +31,7 @@ Every model verb takes ONE options object. The common loop:
|
|
|
31
31
|
|
|
32
32
|
1. **Read** the row — `await ablo.<model>.retrieve({ id })` (async; from the server) or `await ablo.<model>.list({ where })` for many. In React render, read synchronously with `useAblo((a) => a.<model>.get(id))`.
|
|
33
33
|
2. **See who's active** (optional) — `ablo.<model>.claim.state({ id })` (synchronous; never blocks).
|
|
34
|
-
3. **Claim** the row before changing it — `await using claim = await ablo.<model>.claim({ id,
|
|
34
|
+
3. **Claim** the row before changing it — `await using claim = await ablo.<model>.claim({ id, reason?, ttl? })`. If someone else holds it, this waits for them, then gives you the fresh row on `claim.data`. The claim auto-releases when it goes out of scope (`await using`).
|
|
35
35
|
4. **Write** — `await ablo.<model>.update({ id: claim.data.id, data })`. Because you hold the claim, the write is rejected if the row changed underneath you.
|
|
36
36
|
|
|
37
37
|
Keep coding assistants on this schema-backed path.
|
|
@@ -59,7 +59,7 @@ if (!report) throw new Error('Report not found');
|
|
|
59
59
|
// row before resolving. Auto-released at the end of this scope (`await using`).
|
|
60
60
|
await using claim = await ablo.weatherReports.claim({
|
|
61
61
|
id: 'report_stockholm',
|
|
62
|
-
|
|
62
|
+
reason: 'forecasting',
|
|
63
63
|
ttl: '2m',
|
|
64
64
|
});
|
|
65
65
|
const claimed = claim.data;
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Schema authoring: split model routing into two orthogonal axes — `policy` (row access) and `groups` (sync-group routing).
|
|
8
|
+
|
|
9
|
+
**Breaking (schema authoring).** The flat, collision-prone model options are replaced by two namespaced ones:
|
|
10
|
+
- **`policy`** — row-access / tenant isolation (named after Postgres/Supabase RLS policies: the rule that scopes which rows a tenant may read). A discriminated union on `by` replaces the old `orgScoped` / `scopedVia` / `orgColumn` trio:
|
|
11
|
+
- `{ by: 'column' }` — row-local tenancy column (the default when omitted; column name still overridable).
|
|
12
|
+
- `{ by: 'parent', fk, parent }` — inherit tenancy through a foreign key when the table has no tenancy column of its own (e.g. `slide_layers` → `slides`).
|
|
13
|
+
- Type `TenancyInput` is renamed `PolicyInput`; `policyInputSchema` / `resolvePolicy` are now exported.
|
|
14
|
+
- **`groups: { root, grants, roles }`** — which delta channels a row fans into (orthogonal to `policy`, which governs read access). One namespaced object replaces the old flat `scope` / `grants` / `entityRoles`:
|
|
15
|
+
- `root` (was `scope`) — mark a model a scope root; its records form the group `<kind>:<id>`. Renamed so it no longer collides with the old `scopedVia` tenancy sugar or the inner `grants.scope` relation name.
|
|
16
|
+
- `grants` — a membership edge granting an identity access to a scope root.
|
|
17
|
+
- `roles` (was `entityRoles`) — explicit non-relational record→group roles; accepts one role or an array.
|
|
18
|
+
- `groupsInputSchema` / `GroupsInput` are now exported.
|
|
19
|
+
|
|
20
|
+
**CLI.** `config.json` now stores per-project profile key pairs (`profiles: Record<string, ProfileKeys>`) instead of a single top-level pair; older flat layouts are folded into the active profile automatically on read, so existing logins keep working. `login` / `projects` updated to the profile model.
|
|
21
|
+
|
|
3
22
|
## 0.12.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -274,7 +274,7 @@ ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
|
274
274
|
ablo.weatherReports.claim.queue({ id: 'report_stockholm' });
|
|
275
275
|
|
|
276
276
|
{
|
|
277
|
-
await using claim = await ablo.weatherReports.claim({ id,
|
|
277
|
+
await using claim = await ablo.weatherReports.claim({ id, queue: false });
|
|
278
278
|
/* do the held work */
|
|
279
279
|
}
|
|
280
280
|
|
|
@@ -285,11 +285,11 @@ ablo.weatherReports.claim.queue({ id: 'report_stockholm' });
|
|
|
285
285
|
```
|
|
286
286
|
|
|
287
287
|
`claim.state` returns the holder (or `null`); `claim.queue` returns the line waiting
|
|
288
|
-
behind it. `
|
|
288
|
+
behind it. `queue: false` skips rather than waiting when the row is held;
|
|
289
289
|
`maxQueueDepth: 2` bails when two or more are already ahead.
|
|
290
290
|
|
|
291
291
|
Default reads keep working while a row is claimed. Server reads that need claimed
|
|
292
|
-
semantics can opt in with `ifClaimed: 'return' | '
|
|
292
|
+
semantics can opt in with `ifClaimed: 'return' | 'fail'`.
|
|
293
293
|
|
|
294
294
|
Even an unclaimed write can't land on stale reasoning — the commit is guarded:
|
|
295
295
|
|
package/dist/cli.cjs
CHANGED
|
@@ -277023,6 +277023,7 @@ var roleSchema = import_zod2.z.object({
|
|
|
277023
277023
|
kind: import_zod2.z.string().regex(/^[a-z][a-z0-9_]*$/, 'kind must be a lowercase identifier, e.g. "deck"'),
|
|
277024
277024
|
source: roleSourceSchema
|
|
277025
277025
|
});
|
|
277026
|
+
var entityRoleSchema = roleSchema;
|
|
277026
277027
|
var scopeSchema = import_zod2.z.union([
|
|
277027
277028
|
import_zod2.z.boolean(),
|
|
277028
277029
|
import_zod2.z.string().regex(/^[a-z][a-z0-9_]*$/, 'scope kind must be a lowercase identifier, e.g. "dataroom"')
|
|
@@ -277031,6 +277032,11 @@ var grantsRefSchema = import_zod2.z.object({
|
|
|
277031
277032
|
subject: import_zod2.z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, "grants.subject must name a relation"),
|
|
277032
277033
|
scope: import_zod2.z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, "grants.scope must name a relation")
|
|
277033
277034
|
});
|
|
277035
|
+
var groupsInputSchema = import_zod2.z.object({
|
|
277036
|
+
root: scopeSchema.optional(),
|
|
277037
|
+
grants: grantsRefSchema.optional(),
|
|
277038
|
+
roles: import_zod2.z.union([entityRoleSchema, import_zod2.z.array(entityRoleSchema)]).optional()
|
|
277039
|
+
});
|
|
277034
277040
|
|
|
277035
277041
|
// src/coordination/schema.ts
|
|
277036
277042
|
var targetRangeSchema = import_zod3.z.object({
|
|
@@ -279646,6 +279652,7 @@ init_cjs_shims();
|
|
|
279646
279652
|
var import_os2 = require("os");
|
|
279647
279653
|
var import_path2 = require("path");
|
|
279648
279654
|
var import_fs3 = require("fs");
|
|
279655
|
+
var DEFAULT_PROFILE = "default";
|
|
279649
279656
|
function configDir() {
|
|
279650
279657
|
if (process.env.ABLO_CONFIG_DIR) return process.env.ABLO_CONFIG_DIR;
|
|
279651
279658
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
@@ -279657,12 +279664,32 @@ function configPath() {
|
|
|
279657
279664
|
function credentialsPath() {
|
|
279658
279665
|
return (0, import_path2.join)(configDir(), "credentials.json");
|
|
279659
279666
|
}
|
|
279667
|
+
function activeProfileName(cfg) {
|
|
279668
|
+
return cfg.activeProject?.slug ?? DEFAULT_PROFILE;
|
|
279669
|
+
}
|
|
279660
279670
|
function asKeyEntry(value) {
|
|
279661
279671
|
if (value && typeof value === "object" && typeof value.apiKey === "string") {
|
|
279662
279672
|
return value;
|
|
279663
279673
|
}
|
|
279664
279674
|
return void 0;
|
|
279665
279675
|
}
|
|
279676
|
+
function asProfileKeys(value) {
|
|
279677
|
+
if (!value || typeof value !== "object") return void 0;
|
|
279678
|
+
const v = value;
|
|
279679
|
+
const sandbox = asKeyEntry(v.sandbox);
|
|
279680
|
+
const production = asKeyEntry(v.production);
|
|
279681
|
+
if (!sandbox && !production) return void 0;
|
|
279682
|
+
return { ...sandbox ? { sandbox } : {}, ...production ? { production } : {} };
|
|
279683
|
+
}
|
|
279684
|
+
function asProfileMap(value) {
|
|
279685
|
+
if (!value || typeof value !== "object") return {};
|
|
279686
|
+
const out = {};
|
|
279687
|
+
for (const [name, v] of Object.entries(value)) {
|
|
279688
|
+
const keys = asProfileKeys(v);
|
|
279689
|
+
if (keys) out[name] = keys;
|
|
279690
|
+
}
|
|
279691
|
+
return out;
|
|
279692
|
+
}
|
|
279666
279693
|
function readJson(path) {
|
|
279667
279694
|
if (!(0, import_fs3.existsSync)(path)) return null;
|
|
279668
279695
|
try {
|
|
@@ -279683,7 +279710,7 @@ function normalizeStoredMode(value) {
|
|
|
279683
279710
|
if (value === "sandbox" || value === "production") return value;
|
|
279684
279711
|
return void 0;
|
|
279685
279712
|
}
|
|
279686
|
-
function
|
|
279713
|
+
function extractLegacyEntries(obj) {
|
|
279687
279714
|
const sandbox = asKeyEntry(obj.sandbox);
|
|
279688
279715
|
const production = asKeyEntry(obj.production);
|
|
279689
279716
|
if (sandbox || production) {
|
|
@@ -279692,24 +279719,33 @@ function extractEntries(obj) {
|
|
|
279692
279719
|
const flat = asKeyEntry(obj);
|
|
279693
279720
|
return flat ? { sandbox: flat } : {};
|
|
279694
279721
|
}
|
|
279722
|
+
function hasKey(keys) {
|
|
279723
|
+
return !!(keys?.sandbox || keys?.production);
|
|
279724
|
+
}
|
|
279695
279725
|
function readConfig() {
|
|
279696
279726
|
const cfgObj = readJson(configPath());
|
|
279697
279727
|
const credObj = readJson(credentialsPath());
|
|
279698
279728
|
const mode2 = normalizeStoredMode(cfgObj?.mode) ?? normalizeStoredMode(credObj?.mode);
|
|
279699
279729
|
const activeProject = asActiveProject(cfgObj?.activeProject);
|
|
279700
|
-
const
|
|
279701
|
-
const
|
|
279702
|
-
...
|
|
279703
|
-
...
|
|
279704
|
-
// credentials file wins
|
|
279730
|
+
const activeName = activeProject?.slug ?? DEFAULT_PROFILE;
|
|
279731
|
+
const profiles = {
|
|
279732
|
+
...asProfileMap(credObj?.profiles),
|
|
279733
|
+
...asProfileMap(cfgObj?.profiles)
|
|
279705
279734
|
};
|
|
279706
|
-
|
|
279735
|
+
const legacyCfg = cfgObj ? extractLegacyEntries(cfgObj) : {};
|
|
279736
|
+
const legacyCred = credObj ? extractLegacyEntries(credObj) : {};
|
|
279737
|
+
const legacy = { ...legacyCfg, ...legacyCred };
|
|
279738
|
+
const migratedLegacy = hasKey(legacy) && !hasKey(profiles[activeName]);
|
|
279739
|
+
if (migratedLegacy) profiles[activeName] = legacy;
|
|
279740
|
+
const anyKey = Object.values(profiles).some(hasKey);
|
|
279741
|
+
if (!mode2 && !anyKey) return null;
|
|
279707
279742
|
const config = {
|
|
279708
279743
|
mode: mode2 ?? "sandbox",
|
|
279709
279744
|
...activeProject ? { activeProject } : {},
|
|
279710
|
-
|
|
279745
|
+
profiles
|
|
279711
279746
|
};
|
|
279712
|
-
|
|
279747
|
+
const secretsInConfig = hasKey(legacyCfg);
|
|
279748
|
+
if (secretsInConfig || migratedLegacy) writeConfig(config);
|
|
279713
279749
|
return config;
|
|
279714
279750
|
}
|
|
279715
279751
|
function writeConfig(cfg) {
|
|
@@ -279725,16 +279761,34 @@ function writeConfig(cfg) {
|
|
|
279725
279761
|
`,
|
|
279726
279762
|
{ mode: 384 }
|
|
279727
279763
|
);
|
|
279728
|
-
const
|
|
279729
|
-
|
|
279730
|
-
|
|
279731
|
-
|
|
279732
|
-
|
|
279764
|
+
const profiles = {};
|
|
279765
|
+
for (const [name, keys] of Object.entries(cfg.profiles)) {
|
|
279766
|
+
if (!hasKey(keys)) continue;
|
|
279767
|
+
profiles[name] = {
|
|
279768
|
+
...keys.sandbox ? { sandbox: keys.sandbox } : {},
|
|
279769
|
+
...keys.production ? { production: keys.production } : {}
|
|
279770
|
+
};
|
|
279771
|
+
}
|
|
279772
|
+
(0, import_fs3.writeFileSync)(credentialsPath(), `${JSON.stringify({ profiles }, null, 2)}
|
|
279733
279773
|
`, { mode: 384 });
|
|
279734
279774
|
return credentialsPath();
|
|
279735
279775
|
}
|
|
279776
|
+
function emptyConfig(mode2 = "sandbox") {
|
|
279777
|
+
return { mode: mode2, profiles: {} };
|
|
279778
|
+
}
|
|
279779
|
+
function setProfileKeys(profileName, keys, opts) {
|
|
279780
|
+
const cfg = readConfig() ?? emptyConfig(opts.mode);
|
|
279781
|
+
cfg.mode = opts.mode;
|
|
279782
|
+
cfg.profiles[profileName] = {
|
|
279783
|
+
...keys.sandbox ? { sandbox: keys.sandbox } : {},
|
|
279784
|
+
...keys.production ? { production: keys.production } : {}
|
|
279785
|
+
};
|
|
279786
|
+
if (opts.activeProject) cfg.activeProject = opts.activeProject;
|
|
279787
|
+
else delete cfg.activeProject;
|
|
279788
|
+
return writeConfig(cfg);
|
|
279789
|
+
}
|
|
279736
279790
|
function setMode(mode2) {
|
|
279737
|
-
const cfg = readConfig() ??
|
|
279791
|
+
const cfg = readConfig() ?? emptyConfig(mode2);
|
|
279738
279792
|
cfg.mode = mode2;
|
|
279739
279793
|
return writeConfig(cfg);
|
|
279740
279794
|
}
|
|
@@ -279745,13 +279799,15 @@ function getActiveProject() {
|
|
|
279745
279799
|
return readConfig()?.activeProject;
|
|
279746
279800
|
}
|
|
279747
279801
|
function setActiveProject(project) {
|
|
279748
|
-
const cfg = readConfig() ??
|
|
279802
|
+
const cfg = readConfig() ?? emptyConfig("sandbox");
|
|
279749
279803
|
if (project) cfg.activeProject = project;
|
|
279750
279804
|
else delete cfg.activeProject;
|
|
279751
279805
|
return writeConfig(cfg);
|
|
279752
279806
|
}
|
|
279753
279807
|
function getKeyEntry(mode2) {
|
|
279754
|
-
|
|
279808
|
+
const cfg = readConfig();
|
|
279809
|
+
if (!cfg) return void 0;
|
|
279810
|
+
return cfg.profiles[activeProfileName(cfg)]?.[mode2];
|
|
279755
279811
|
}
|
|
279756
279812
|
function modeFromKey(key) {
|
|
279757
279813
|
if (/^(sk|rk)_test_/.test(key)) return "sandbox";
|
|
@@ -279775,11 +279831,21 @@ function resolveApiKey(modeOverride) {
|
|
|
279775
279831
|
if (process.env.ABLO_API_KEY) return process.env.ABLO_API_KEY;
|
|
279776
279832
|
const cfg = readConfig();
|
|
279777
279833
|
if (!cfg) return void 0;
|
|
279778
|
-
const entry = cfg[modeOverride ?? cfg.mode];
|
|
279834
|
+
const entry = cfg.profiles[activeProfileName(cfg)]?.[modeOverride ?? cfg.mode];
|
|
279779
279835
|
if (!entry) return void 0;
|
|
279780
279836
|
if (entry.expiresAt && Date.parse(entry.expiresAt) <= Date.now()) return void 0;
|
|
279781
279837
|
return entry.apiKey;
|
|
279782
279838
|
}
|
|
279839
|
+
function guardActiveProjectKey() {
|
|
279840
|
+
if (process.env.ABLO_API_KEY) {
|
|
279841
|
+
return { ok: true, activeProfile: DEFAULT_PROFILE, available: [] };
|
|
279842
|
+
}
|
|
279843
|
+
const cfg = readConfig();
|
|
279844
|
+
const activeProfile = cfg ? activeProfileName(cfg) : DEFAULT_PROFILE;
|
|
279845
|
+
const profiles = cfg?.profiles ?? {};
|
|
279846
|
+
const available = Object.entries(profiles).filter(([, keys]) => hasKey(keys)).map(([name]) => name);
|
|
279847
|
+
return { ok: hasKey(profiles[activeProfile]), activeProfile, available };
|
|
279848
|
+
}
|
|
279783
279849
|
function resolvePushPlan() {
|
|
279784
279850
|
const envKey = process.env.ABLO_API_KEY;
|
|
279785
279851
|
if (envKey) return { flow: modeFromKey(envKey) ?? getMode(), apiKey: envKey, source: "env" };
|
|
@@ -280425,8 +280491,19 @@ function openBrowser(url) {
|
|
|
280425
280491
|
} catch {
|
|
280426
280492
|
}
|
|
280427
280493
|
}
|
|
280428
|
-
|
|
280494
|
+
function parseProjectFlag(argv) {
|
|
280495
|
+
const i = argv.indexOf("--project");
|
|
280496
|
+
if (i >= 0) {
|
|
280497
|
+
const slug = argv[i + 1];
|
|
280498
|
+
if (slug && !slug.startsWith("-")) return slug;
|
|
280499
|
+
}
|
|
280500
|
+
const eq = argv.find((a) => a.startsWith("--project="));
|
|
280501
|
+
return eq ? eq.slice("--project=".length) || void 0 : void 0;
|
|
280502
|
+
}
|
|
280503
|
+
async function deviceLogin(argv) {
|
|
280429
280504
|
Ie(`${brand("ablo")} login`);
|
|
280505
|
+
const requested = parseProjectFlag(argv) ?? getActiveProject()?.slug;
|
|
280506
|
+
const targetProject = requested === DEFAULT_PROFILE ? void 0 : requested;
|
|
280430
280507
|
const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
280431
280508
|
let account = "login";
|
|
280432
280509
|
if (interactive) {
|
|
@@ -280504,13 +280581,19 @@ ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
|
|
|
280504
280581
|
s.stop("Timed out waiting for approval.");
|
|
280505
280582
|
process.exit(1);
|
|
280506
280583
|
}
|
|
280507
|
-
s.message(
|
|
280584
|
+
s.message(
|
|
280585
|
+
targetProject ? `Provisioning keys for ${targetProject}\u2026` : "Provisioning a sandbox key\u2026"
|
|
280586
|
+
);
|
|
280508
280587
|
const provRes = await fetch(`${AUTH_URL}/api/cli/provision-key`, {
|
|
280509
280588
|
method: "POST",
|
|
280510
280589
|
headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json" },
|
|
280511
|
-
//
|
|
280512
|
-
//
|
|
280513
|
-
|
|
280590
|
+
// Scope the minted keys to the chosen project (`--project`/active), with
|
|
280591
|
+
// the device_code as a legacy fallback for the /cli picker. Both harmless
|
|
280592
|
+
// if absent → org-default keys.
|
|
280593
|
+
body: JSON.stringify({
|
|
280594
|
+
device_code: code.device_code,
|
|
280595
|
+
...targetProject ? { project_slug: targetProject } : {}
|
|
280596
|
+
})
|
|
280514
280597
|
}).catch(() => null);
|
|
280515
280598
|
if (!provRes || !provRes.ok) {
|
|
280516
280599
|
s.stop("Could not provision a key.");
|
|
@@ -280528,16 +280611,23 @@ ${import_picocolors7.default.dim(url)}`, "Approve in your browser");
|
|
|
280528
280611
|
...prov.organizationId ? { organizationId: prov.organizationId } : {},
|
|
280529
280612
|
...k3.expiresAt ? { expiresAt: k3.expiresAt } : {}
|
|
280530
280613
|
});
|
|
280531
|
-
const
|
|
280532
|
-
|
|
280533
|
-
|
|
280534
|
-
|
|
280535
|
-
|
|
280614
|
+
const profileName = prov.project?.slug ?? DEFAULT_PROFILE;
|
|
280615
|
+
const path = setProfileKeys(
|
|
280616
|
+
profileName,
|
|
280617
|
+
{
|
|
280618
|
+
sandbox: entry(prov.test),
|
|
280619
|
+
...prov.live ? { production: entry(prov.live) } : {}
|
|
280620
|
+
},
|
|
280621
|
+
{ mode: "sandbox", activeProject: prov.project ?? void 0 }
|
|
280622
|
+
);
|
|
280536
280623
|
s.stop(`Saved keys to ${path}`);
|
|
280537
|
-
|
|
280624
|
+
const where = prov.project ? ` ${import_picocolors7.default.dim(`(project ${prov.project.slug})`)}` : "";
|
|
280625
|
+
Se(
|
|
280626
|
+
`${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.`
|
|
280627
|
+
);
|
|
280538
280628
|
}
|
|
280539
|
-
async function login() {
|
|
280540
|
-
await deviceLogin();
|
|
280629
|
+
async function login(argv = []) {
|
|
280630
|
+
await deviceLogin(argv);
|
|
280541
280631
|
}
|
|
280542
280632
|
function logout() {
|
|
280543
280633
|
const removed = clearCredential();
|
|
@@ -280790,11 +280880,13 @@ async function projects(argv) {
|
|
|
280790
280880
|
setActiveProject({ id: target.id, slug: target.slug });
|
|
280791
280881
|
console.log(` ${import_picocolors9.default.green("\u2713")} now targeting project ${import_picocolors9.default.bold(target.slug)} ${import_picocolors9.default.dim(`(${target.id})`)}`);
|
|
280792
280882
|
}
|
|
280793
|
-
|
|
280794
|
-
|
|
280795
|
-
|
|
280796
|
-
|
|
280797
|
-
|
|
280883
|
+
const guard = guardActiveProjectKey();
|
|
280884
|
+
if (!guard.ok) {
|
|
280885
|
+
const loginCmd = guard.activeProfile === DEFAULT_PROFILE ? "ablo login" : `ablo login --project ${guard.activeProfile}`;
|
|
280886
|
+
console.log(
|
|
280887
|
+
import_picocolors9.default.dim(` No key stored for this project yet \u2014 run ${import_picocolors9.default.bold(loginCmd)} to mint one.`)
|
|
280888
|
+
);
|
|
280889
|
+
}
|
|
280798
280890
|
return;
|
|
280799
280891
|
}
|
|
280800
280892
|
console.error(
|
|
@@ -281370,7 +281462,7 @@ function spreadOpts(optsArg) {
|
|
|
281370
281462
|
}
|
|
281371
281463
|
return `, ...${optsArg.getText()}`;
|
|
281372
281464
|
}
|
|
281373
|
-
function
|
|
281465
|
+
function hasKey2(obj, key) {
|
|
281374
281466
|
return obj.getProperties().some(
|
|
281375
281467
|
(p2) => (import_ts_morph.Node.isPropertyAssignment(p2) || import_ts_morph.Node.isShorthandPropertyAssignment(p2)) && p2.getName() === key
|
|
281376
281468
|
);
|
|
@@ -281383,7 +281475,7 @@ function verbRewrite(call, verb) {
|
|
|
281383
281475
|
const first = args[0];
|
|
281384
281476
|
const calleeText = call.getExpression().getText();
|
|
281385
281477
|
if (verb === "create") {
|
|
281386
|
-
if (import_ts_morph.Node.isObjectLiteralExpression(first) &&
|
|
281478
|
+
if (import_ts_morph.Node.isObjectLiteralExpression(first) && hasKey2(first, "data")) return null;
|
|
281387
281479
|
return `${calleeText}({ data: ${first.getText()}${spreadOpts(args[1])} })`;
|
|
281388
281480
|
}
|
|
281389
281481
|
if (import_ts_morph.Node.isObjectLiteralExpression(first)) return null;
|
|
@@ -282212,7 +282304,7 @@ async function main() {
|
|
|
282212
282304
|
if (command === "init") {
|
|
282213
282305
|
await init(process.argv.slice(3));
|
|
282214
282306
|
} else if (command === "login") {
|
|
282215
|
-
await login();
|
|
282307
|
+
await login(process.argv.slice(3));
|
|
282216
282308
|
} else if (command === "logout") {
|
|
282217
282309
|
logout();
|
|
282218
282310
|
} else if (command === "mode") {
|
|
@@ -282251,6 +282343,22 @@ async function main() {
|
|
|
282251
282343
|
const rest = process.argv.slice(3);
|
|
282252
282344
|
const advanced = rest.some((a) => ["--force", "--rename", "--backfill", "--url"].includes(a));
|
|
282253
282345
|
const watching = rest.includes("--watch");
|
|
282346
|
+
const guard = guardActiveProjectKey();
|
|
282347
|
+
if (!guard.ok && guard.available.length > 0 && !rest.includes("--url")) {
|
|
282348
|
+
console.error(
|
|
282349
|
+
` ${import_picocolors18.default.yellow("\u26A0")} active project ${import_picocolors18.default.bold(guard.activeProfile)} has no stored key ${import_picocolors18.default.dim(
|
|
282350
|
+
`(you have keys for: ${guard.available.join(", ")})`
|
|
282351
|
+
)}`
|
|
282352
|
+
);
|
|
282353
|
+
const loginCmd = guard.activeProfile === "default" ? "ablo login" : `ablo login --project ${guard.activeProfile}`;
|
|
282354
|
+
console.error(
|
|
282355
|
+
import_picocolors18.default.dim(
|
|
282356
|
+
` Mint one with ${import_picocolors18.default.bold(loginCmd)}, or switch with ${import_picocolors18.default.bold("ablo projects use <slug>")}.`
|
|
282357
|
+
)
|
|
282358
|
+
);
|
|
282359
|
+
process.exitCode = 1;
|
|
282360
|
+
return;
|
|
282361
|
+
}
|
|
282254
282362
|
const plan = resolvePushPlan();
|
|
282255
282363
|
if (advanced || plan.flow === "production" && !watching) {
|
|
282256
282364
|
await push(rest);
|
|
@@ -282275,11 +282383,12 @@ async function main() {
|
|
|
282275
282383
|
console.log(` [--auth apikey] [--storage direct|endpoint] [--project <slug>] [--no-project]`);
|
|
282276
282384
|
console.log(` [--no-agent] [--no-pull] [--no-install] [--no-login]`);
|
|
282277
282385
|
console.log(` npx ablo login Authorize in your browser (provisions sandbox + production keys)`);
|
|
282386
|
+
console.log(` npx ablo login --project <slug> Same, scoped to a project (mints its keys, makes it active)`);
|
|
282278
282387
|
console.log(` npx ablo logout Remove the stored API key`);
|
|
282279
282388
|
console.log(` npx ablo mode [sandbox|production] Switch active environment, like Stripe`);
|
|
282280
282389
|
console.log(` npx ablo projects list List the org's projects (default + your own)`);
|
|
282281
282390
|
console.log(` npx ablo projects create <slug> Create a project (its keys/schema/data are isolated)`);
|
|
282282
|
-
console.log(` npx ablo projects use <slug|default>
|
|
282391
|
+
console.log(` npx ablo projects use <slug|default> Switch the active project (run login --project to mint its keys)`);
|
|
282283
282392
|
console.log(` npx ablo status Show org, mode, keys, and server health`);
|
|
282284
282393
|
console.log(` npx ablo status --json Same, machine-readable (mode, key prefix, org id, api host)`);
|
|
282285
282394
|
console.log(` npx ablo logs [-n N] [--since 15m] Tail commit activity (follows; --no-follow to exit)`);
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
export { z } from 'zod';
|
|
24
24
|
export { field, indexed, getFieldMeta, type FieldBuilder, type FieldMeta } from './field.js';
|
|
25
25
|
export { relation, type RelationDef, type RelationType } from './relation.js';
|
|
26
|
-
export { tenancySchema, scopedViaRefSchema, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, type Tenancy, type ScopedViaRef, type
|
|
26
|
+
export { tenancySchema, scopedViaRefSchema, policyInputSchema, resolvePolicy, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, type Tenancy, type ScopedViaRef, type PolicyInput, } from './tenancy.js';
|
|
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
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 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';
|
|
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, groupsInputSchema, type GroupsInput, } 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';
|
package/dist/schema/index.js
CHANGED
|
@@ -27,7 +27,7 @@ export { field, indexed, getFieldMeta } from './field.js';
|
|
|
27
27
|
// Relation builders
|
|
28
28
|
export { relation } from './relation.js';
|
|
29
29
|
// Tenancy — the single source of truth for how a model's rows are tenant-scoped.
|
|
30
|
-
export { tenancySchema, scopedViaRefSchema, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, } from './tenancy.js';
|
|
30
|
+
export { tenancySchema, scopedViaRefSchema, policyInputSchema, resolvePolicy, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, } from './tenancy.js';
|
|
31
31
|
// Database plane — which DB a model's rows live in (`tenant` portable to a BYO
|
|
32
32
|
// customer DB, `control` = Ablo's own). Sibling axis to `tenancy`.
|
|
33
33
|
export { planeSchema, DEFAULT_PLANE } from './plane.js';
|
|
@@ -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, syncGroupInputSchema, isSyncGroupInput, 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, groupsInputSchema, } 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.
|
package/dist/schema/model.d.ts
CHANGED
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { z } from 'zod';
|
|
20
20
|
import type { RelationDef } from './relation.js';
|
|
21
|
-
import type { EntityRole } from './roles.js';
|
|
21
|
+
import type { EntityRole, GroupsInput } from './roles.js';
|
|
22
22
|
import { type FieldMeta } from './field.js';
|
|
23
|
-
import { type Tenancy, type
|
|
24
|
-
export type { ScopedViaRef, Tenancy } from './tenancy.js';
|
|
23
|
+
import { type Tenancy, type PolicyInput } from './tenancy.js';
|
|
24
|
+
export type { ScopedViaRef, Tenancy, PolicyInput } from './tenancy.js';
|
|
25
25
|
import { type SchemaPlane } from './plane.js';
|
|
26
26
|
/**
|
|
27
27
|
* Controls when model data is loaded from the server.
|
|
@@ -95,49 +95,27 @@ export interface ModelOptions {
|
|
|
95
95
|
*/
|
|
96
96
|
tableName?: string;
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* tenant
|
|
98
|
+
* **Axis 1 — row-access policy (tenant isolation / RLS).** Decides who may
|
|
99
|
+
* *read* a row at all. Named after Postgres/Supabase, where a `policy` is the
|
|
100
|
+
* rule that scopes which rows a tenant sees. A discriminated union on `by` —
|
|
101
|
+
* one option replacing the old `orgScoped`/`scopedVia`/`orgColumn` trio:
|
|
101
102
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* `teamMember.team_id → team.id`, `users.id ← member.user_id`).
|
|
115
|
-
*
|
|
116
|
-
* Emits, in place of the missing `organization_id = $1` clause:
|
|
117
|
-
*
|
|
118
|
-
* WHERE <table>.<localKey> IN
|
|
119
|
-
* (SELECT <parentKey> FROM <parentTable> WHERE <parentOrgColumn> = $1)
|
|
120
|
-
*
|
|
121
|
-
* Use this INSTEAD of `orgScoped: false` for any `load: 'instant'`
|
|
122
|
-
* model whose rows would otherwise leak cross-tenant on bootstrap —
|
|
123
|
-
* dropping the filter entirely exposes the entire DB to every client.
|
|
103
|
+
* - `{ by: 'column' }` — row-local tenancy column (the DEFAULT when omitted).
|
|
104
|
+
* `column` overrides the name (default `organization_id`).
|
|
105
|
+
* - `{ by: 'parent', fk, parent }` — inherit tenancy through a foreign key
|
|
106
|
+
* when THIS table has no tenancy column of its own (e.g. `slide_layers` →
|
|
107
|
+
* slide → deck → org). Emits, in place of `organization_id = $1`:
|
|
108
|
+
* `WHERE <table>.<fk> IN (SELECT <parentKey> FROM <parent> WHERE
|
|
109
|
+
* <parentTenantColumn> = $1)`. Use this for any `load: 'instant'` child
|
|
110
|
+
* table that would otherwise leak cross-tenant on bootstrap.
|
|
111
|
+
* - `{ by: 'none' }` — genuinely global / reference data (the `organizations`
|
|
112
|
+
* table itself, global lookups). ⚠ Makes the whole table readable
|
|
113
|
+
* cross-tenant — only correct for tenant-less tables. Because it's an
|
|
114
|
+
* explicit, named branch (not a falsy flag) it can't be reached by accident.
|
|
124
115
|
*
|
|
125
|
-
*
|
|
126
|
-
* the SQL compiler validates them to keep this away from injection
|
|
127
|
-
* paths.
|
|
116
|
+
* Normalized into the canonical {@link Tenancy} by `resolvePolicy` at build.
|
|
128
117
|
*/
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Override the physical tenancy column name (default `organization_id`).
|
|
132
|
-
* Authoring sugar — normalized into the canonical {@link tenancy} descriptor.
|
|
133
|
-
*/
|
|
134
|
-
orgColumn?: string;
|
|
135
|
-
/**
|
|
136
|
-
* Canonical tenancy descriptor. You normally don't set this — `orgScoped`,
|
|
137
|
-
* `scopedVia`, and `orgColumn` are normalized into it at build time. Set it
|
|
138
|
-
* directly only to author the union form explicitly.
|
|
139
|
-
*/
|
|
140
|
-
tenancy?: Tenancy;
|
|
118
|
+
policy?: PolicyInput;
|
|
141
119
|
/**
|
|
142
120
|
* Which database plane this model's rows live in. `tenant` (default) =
|
|
143
121
|
* the tenant data plane, emitted into a customer's BYO/dedicated DB by
|
|
@@ -146,53 +124,29 @@ export interface ModelOptions {
|
|
|
146
124
|
*/
|
|
147
125
|
plane?: SchemaPlane;
|
|
148
126
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* string to override the kind explicitly (`scope: 'matter'`).
|
|
153
|
-
*
|
|
154
|
-
* Replaces the old `syncGroupFormat` template string: there is no `{id}`
|
|
155
|
-
* placeholder to author — the engine mints the branded {@link SyncGroup} from
|
|
156
|
-
* `(kind, id)`. Child models inherit a root's group via their `belongsTo`
|
|
157
|
-
* relations (a `document` with `dataroomId` fans into `dataroom:<id>`), so
|
|
158
|
-
* only the root declares anything.
|
|
159
|
-
*/
|
|
160
|
-
scope?: boolean | string;
|
|
161
|
-
/**
|
|
162
|
-
* Declares this model as a **membership edge** that grants an identity access
|
|
163
|
-
* to a scope root — the relation-driven equivalent of "this user can see that
|
|
164
|
-
* dataroom." Both values are *relation names* already declared on this model:
|
|
165
|
-
* `subject` is the `belongsTo` to the identity (e.g. a `user`), `scope` is the
|
|
166
|
-
* `belongsTo` to the scope-root entity (e.g. a `dataroom`).
|
|
127
|
+
* **Axis 2 — sync-group routing.** Decides which delta *channels* a row fans
|
|
128
|
+
* into. Orthogonal to {@link policy} (read access). One namespaced object
|
|
129
|
+
* replacing the old flat `scope`/`grants`/`entityRoles`:
|
|
167
130
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
131
|
+
* - `root` — mark this model a scope root; its records form the group
|
|
132
|
+
* `<kind>:<id>` (kind defaults from the lowercased typename, e.g. `Deck` →
|
|
133
|
+
* `deck:<id>`; pass a string to override, `root: 'matter'`). Child models
|
|
134
|
+
* inherit a root's group via their `belongsTo` relations. Was `scope` —
|
|
135
|
+
* renamed so it no longer collides with the old `scopedVia` tenancy sugar.
|
|
136
|
+
* - `grants` — a membership edge granting an identity access to a scope root.
|
|
137
|
+
* Both values name `belongsTo` relations on this model (`subject` → identity,
|
|
138
|
+
* `scope` → scope root). Only needed for sub-org sharing.
|
|
139
|
+
* - `roles` — explicit non-relational record→group roles (the inbox-fan-out
|
|
140
|
+
* escape hatch, keyed on a plain field). Was `entityRoles`. One or many.
|
|
171
141
|
*
|
|
172
142
|
* ```ts
|
|
173
143
|
* // dataroomMember: { userId, dataroomId }
|
|
174
|
-
* grants: { subject: 'user', scope: 'dataroom' }
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* Not needed for org-level access — a `dataroom.organizationId` already routes
|
|
178
|
-
* via the `org:<id>` group. `grants` is only for sub-org membership.
|
|
179
|
-
*/
|
|
180
|
-
grants?: GrantsRef;
|
|
181
|
-
/**
|
|
182
|
-
* Explicit record→group roles for routing that isn't relational — a group
|
|
183
|
-
* keyed on a plain field rather than the record's own id or a `belongsTo`
|
|
184
|
-
* scope root. The escape hatch for cases like per-recipient inbox fan-out:
|
|
185
|
-
*
|
|
186
|
-
* ```ts
|
|
187
|
-
* // a message routes to its addressee's inbox, keyed on `toId`
|
|
188
|
-
* entityRoles: [entityRole({ kind: 'inbox', source: 'toId' })]
|
|
144
|
+
* groups: { grants: { subject: 'user', scope: 'dataroom' } }
|
|
145
|
+
* // a message → its addressee's inbox, keyed on `toId`
|
|
146
|
+
* groups: { roles: [entityRole({ kind: 'inbox', source: 'toId' })] }
|
|
189
147
|
* ```
|
|
190
|
-
*
|
|
191
|
-
* Prefer {@link scope} (self group) + `belongsTo` relations (parent groups)
|
|
192
|
-
* when the routing follows the relation graph — reach for `entityRoles` only
|
|
193
|
-
* when it genuinely doesn't.
|
|
194
148
|
*/
|
|
195
|
-
|
|
149
|
+
groups?: GroupsInput;
|
|
196
150
|
/**
|
|
197
151
|
* Whether clients may issue CREATE/UPDATE/DELETE mutations for this
|
|
198
152
|
* model via the `commit` wire protocol. Default: **true** — declaring a
|