@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/dist/schema/model.js
CHANGED
|
@@ -20,8 +20,10 @@ import { z } from 'zod';
|
|
|
20
20
|
import { getFieldMeta, inferFieldMetaFromZod } from './field.js';
|
|
21
21
|
// Tenancy is owned by `tenancy.ts` (single source of truth). `ScopedViaRef` is
|
|
22
22
|
// re-exported so existing `import { ScopedViaRef } from './model'` call sites
|
|
23
|
-
// keep resolving
|
|
24
|
-
|
|
23
|
+
// keep resolving. Authoring uses the `policy` option (`PolicyInput`, named for
|
|
24
|
+
// Postgres/Supabase RLS), normalized to the canonical `Tenancy` by
|
|
25
|
+
// `resolvePolicy` at build time.
|
|
26
|
+
import { resolvePolicy } from './tenancy.js';
|
|
25
27
|
import { DEFAULT_PLANE } from './plane.js';
|
|
26
28
|
/** Normalize the `entityRoles` option (single | array | undefined) to an array. */
|
|
27
29
|
function normalizeEntityRoles(input) {
|
|
@@ -88,17 +90,15 @@ export function model(shape, relations, options) {
|
|
|
88
90
|
typename: options?.typename,
|
|
89
91
|
persist: options?.persist,
|
|
90
92
|
tableName: options?.tableName,
|
|
91
|
-
//
|
|
92
|
-
tenancy
|
|
93
|
-
|
|
94
|
-
orgScoped: options?.orgScoped,
|
|
95
|
-
scopedVia: options?.scopedVia,
|
|
96
|
-
orgColumn: options?.orgColumn,
|
|
97
|
-
}),
|
|
93
|
+
// Axis 1 — normalize the `policy` authoring option into the one canonical
|
|
94
|
+
// tenancy descriptor (defaults to a row-local org column).
|
|
95
|
+
tenancy: resolvePolicy(options?.policy),
|
|
98
96
|
plane: options?.plane ?? DEFAULT_PLANE,
|
|
99
|
-
|
|
100
|
-
grants
|
|
101
|
-
|
|
97
|
+
// Axis 2 — unpack the `groups` routing namespace into the wire fields the
|
|
98
|
+
// server reads (`scope`/`grants`/`entityRoles` on ModelDef/ModelJSON).
|
|
99
|
+
scope: options?.groups?.root,
|
|
100
|
+
grants: options?.groups?.grants,
|
|
101
|
+
entityRoles: normalizeEntityRoles(options?.groups?.roles),
|
|
102
102
|
mutable: options?.mutable ?? true,
|
|
103
103
|
lazyObservable: options?.lazyObservable,
|
|
104
104
|
computed: options?.computed,
|
package/dist/schema/roles.d.ts
CHANGED
|
@@ -137,6 +137,55 @@ export declare const grantsRefSchema: z.ZodObject<{
|
|
|
137
137
|
subject: z.ZodString;
|
|
138
138
|
scope: z.ZodString;
|
|
139
139
|
}, z.core.$strip>;
|
|
140
|
+
/**
|
|
141
|
+
* The AUTHORING form of a model's sync-group routing — the `groups: { ... }`
|
|
142
|
+
* option. One namespaced object collects the three independent routing knobs
|
|
143
|
+
* that used to be flat, collision-prone model options (`scope` / `grants` /
|
|
144
|
+
* `entityRoles`). Distinct axis from `policy` (tenant isolation): the policy
|
|
145
|
+
* decides who may *read* a row, `groups` decides which delta *channels* a row
|
|
146
|
+
* fans into.
|
|
147
|
+
*
|
|
148
|
+
* - `root` — mark this model a scope root; its records form `<kind>:<id>`
|
|
149
|
+
* (kind defaults from typename). Was the flat `scope` option — renamed to
|
|
150
|
+
* `root` so it no longer collides with the (now removed) `scopedVia` tenancy
|
|
151
|
+
* sugar or the inner `grants.scope` relation name.
|
|
152
|
+
* - `grants` — a membership edge granting an identity access to a scope root.
|
|
153
|
+
* - `roles` — explicit non-relational record→group roles (e.g. inbox fan-out
|
|
154
|
+
* keyed on a plain field). Was `entityRoles`. Accepts one role or an array.
|
|
155
|
+
*/
|
|
156
|
+
export declare const groupsInputSchema: z.ZodObject<{
|
|
157
|
+
root: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>;
|
|
158
|
+
grants: z.ZodOptional<z.ZodObject<{
|
|
159
|
+
subject: z.ZodString;
|
|
160
|
+
scope: z.ZodString;
|
|
161
|
+
}, z.core.$strip>>;
|
|
162
|
+
roles: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<{
|
|
163
|
+
kind: string;
|
|
164
|
+
source: {
|
|
165
|
+
field: string;
|
|
166
|
+
multi: boolean;
|
|
167
|
+
};
|
|
168
|
+
}, unknown, z.core.$ZodTypeInternals<{
|
|
169
|
+
kind: string;
|
|
170
|
+
source: {
|
|
171
|
+
field: string;
|
|
172
|
+
multi: boolean;
|
|
173
|
+
};
|
|
174
|
+
}, unknown>>, z.ZodArray<z.ZodType<{
|
|
175
|
+
kind: string;
|
|
176
|
+
source: {
|
|
177
|
+
field: string;
|
|
178
|
+
multi: boolean;
|
|
179
|
+
};
|
|
180
|
+
}, unknown, z.core.$ZodTypeInternals<{
|
|
181
|
+
kind: string;
|
|
182
|
+
source: {
|
|
183
|
+
field: string;
|
|
184
|
+
multi: boolean;
|
|
185
|
+
};
|
|
186
|
+
}, unknown>>>]>>;
|
|
187
|
+
}, z.core.$strip>;
|
|
188
|
+
export type GroupsInput = z.infer<typeof groupsInputSchema>;
|
|
140
189
|
/** Build an identity-anchored role. `multi` defaults to `false`. */
|
|
141
190
|
export declare function identityRole(spec: {
|
|
142
191
|
readonly kind: string;
|
package/dist/schema/roles.js
CHANGED
|
@@ -112,6 +112,27 @@ export const grantsRefSchema = z.object({
|
|
|
112
112
|
subject: z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, 'grants.subject must name a relation'),
|
|
113
113
|
scope: z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/, 'grants.scope must name a relation'),
|
|
114
114
|
});
|
|
115
|
+
/**
|
|
116
|
+
* The AUTHORING form of a model's sync-group routing — the `groups: { ... }`
|
|
117
|
+
* option. One namespaced object collects the three independent routing knobs
|
|
118
|
+
* that used to be flat, collision-prone model options (`scope` / `grants` /
|
|
119
|
+
* `entityRoles`). Distinct axis from `policy` (tenant isolation): the policy
|
|
120
|
+
* decides who may *read* a row, `groups` decides which delta *channels* a row
|
|
121
|
+
* fans into.
|
|
122
|
+
*
|
|
123
|
+
* - `root` — mark this model a scope root; its records form `<kind>:<id>`
|
|
124
|
+
* (kind defaults from typename). Was the flat `scope` option — renamed to
|
|
125
|
+
* `root` so it no longer collides with the (now removed) `scopedVia` tenancy
|
|
126
|
+
* sugar or the inner `grants.scope` relation name.
|
|
127
|
+
* - `grants` — a membership edge granting an identity access to a scope root.
|
|
128
|
+
* - `roles` — explicit non-relational record→group roles (e.g. inbox fan-out
|
|
129
|
+
* keyed on a plain field). Was `entityRoles`. Accepts one role or an array.
|
|
130
|
+
*/
|
|
131
|
+
export const groupsInputSchema = z.object({
|
|
132
|
+
root: scopeSchema.optional(),
|
|
133
|
+
grants: grantsRefSchema.optional(),
|
|
134
|
+
roles: z.union([entityRoleSchema, z.array(entityRoleSchema)]).optional(),
|
|
135
|
+
});
|
|
115
136
|
// ── Factories ───────────────────────────────────────────────────────────────
|
|
116
137
|
function makeRole(spec) {
|
|
117
138
|
return { kind: spec.kind, source: { field: spec.source, multi: spec.multi ?? false } };
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { z } from 'zod';
|
|
|
23
23
|
import type { ModelDef, RelationRecord } from './model.js';
|
|
24
24
|
import type { RelationDef } from './relation.js';
|
|
25
25
|
import type { IdentityRole } from './roles.js';
|
|
26
|
-
export { type IdentityRole, type IdentityRoleSource, type IdentityContext, type EntityRole, type EntityRoleSource, type EntityContext, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './roles.js';
|
|
26
|
+
export { type IdentityRole, type IdentityRoleSource, type IdentityContext, type EntityRole, type EntityRoleSource, type EntityContext, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './roles.js';
|
|
27
27
|
/** The set of built-in casing conventions supported by `defineSchema`. */
|
|
28
28
|
export type CasingConvention = 'snake_case' | 'camelCase';
|
|
29
29
|
/** Plug point for custom conventions (e.g. mixed legacy databases). */
|
package/dist/schema/schema.js
CHANGED
|
@@ -25,7 +25,7 @@ import { scopeSchema, grantsRefSchema } from './roles.js';
|
|
|
25
25
|
// Sync-group roles (identity + entity) live in `./roles.js`. Re-exported here
|
|
26
26
|
// so the long-standing `@ablo/schema` / `./schema.js` import paths keep working
|
|
27
27
|
// after the rehome — see roles.ts for the full vocabulary.
|
|
28
|
-
export { identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './roles.js';
|
|
28
|
+
export { identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, } from './roles.js';
|
|
29
29
|
function resolveCasing(fn) {
|
|
30
30
|
if (fn === undefined)
|
|
31
31
|
return (x) => x;
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
*
|
|
14
14
|
* What round-trips:
|
|
15
15
|
* - all model routing/scoping metadata (typename, tableName, load,
|
|
16
|
-
* mutable,
|
|
17
|
-
* entityRoles, persist, autoFill, requiredFields, lazyObservable)
|
|
16
|
+
* mutable, the canonical `tenancy` descriptor, bootstrap hints, scope,
|
|
17
|
+
* grants, entityRoles, persist, autoFill, requiredFields, lazyObservable).
|
|
18
|
+
* NOTE: the authoring sugar (`policy`/`groups`) is normalized away at
|
|
19
|
+
* `model()`-build; only the canonical wire fields cross here.
|
|
18
20
|
* - relations (incl. resolved `foreignKeyColumn`)
|
|
19
21
|
* - field metadata (names + type tags), from which validators are rebuilt
|
|
20
22
|
* - identity roles (already pure data)
|
package/dist/schema/serialize.js
CHANGED
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
*
|
|
14
14
|
* What round-trips:
|
|
15
15
|
* - all model routing/scoping metadata (typename, tableName, load,
|
|
16
|
-
* mutable,
|
|
17
|
-
* entityRoles, persist, autoFill, requiredFields, lazyObservable)
|
|
16
|
+
* mutable, the canonical `tenancy` descriptor, bootstrap hints, scope,
|
|
17
|
+
* grants, entityRoles, persist, autoFill, requiredFields, lazyObservable).
|
|
18
|
+
* NOTE: the authoring sugar (`policy`/`groups`) is normalized away at
|
|
19
|
+
* `model()`-build; only the canonical wire fields cross here.
|
|
18
20
|
* - relations (incl. resolved `foreignKeyColumn`)
|
|
19
21
|
* - field metadata (names + type tags), from which validators are rebuilt
|
|
20
22
|
* - identity roles (already pure data)
|
package/dist/schema/sugar.d.ts
CHANGED
|
@@ -63,37 +63,16 @@ export interface SugarOptions<R extends RelationRecord = RelationRecord, C exten
|
|
|
63
63
|
*/
|
|
64
64
|
tableName?: string;
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
66
|
+
* Row-access policy (tenant isolation / RLS) — who may *read* a row.
|
|
67
|
+
* Discriminated union on `by` (`column` | `parent` | `none`). See
|
|
68
|
+
* {@link ModelOptions.policy}.
|
|
68
69
|
*/
|
|
69
|
-
|
|
70
|
+
policy?: ModelOptions['policy'];
|
|
70
71
|
/**
|
|
71
|
-
*
|
|
72
|
-
* `
|
|
72
|
+
* Sync-group routing — which delta *channels* a row fans into
|
|
73
|
+
* (`root` / `grants` / `roles`). See {@link ModelOptions.groups}.
|
|
73
74
|
*/
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Override the row-local tenancy column name. See
|
|
77
|
-
* {@link ModelOptions.orgColumn}.
|
|
78
|
-
*/
|
|
79
|
-
orgColumn?: ModelOptions['orgColumn'];
|
|
80
|
-
/** Canonical tenancy descriptor. See {@link ModelOptions.tenancy}. */
|
|
81
|
-
tenancy?: ModelOptions['tenancy'];
|
|
82
|
-
/**
|
|
83
|
-
* Mark this model a scope root — its records form the group `<kind>:<id>`
|
|
84
|
-
* (kind defaults from typename). See {@link ModelOptions.scope}.
|
|
85
|
-
*/
|
|
86
|
-
scope?: ModelOptions['scope'];
|
|
87
|
-
/**
|
|
88
|
-
* Membership edge granting identity → scope-root access. Both fields are
|
|
89
|
-
* relation names on this model. See {@link ModelOptions.grants}.
|
|
90
|
-
*/
|
|
91
|
-
grants?: ModelOptions['grants'];
|
|
92
|
-
/**
|
|
93
|
-
* Explicit non-relational record→group roles (e.g. inbox fan-out keyed on a
|
|
94
|
-
* field). See {@link ModelOptions.entityRoles}.
|
|
95
|
-
*/
|
|
96
|
-
entityRoles?: ModelOptions['entityRoles'];
|
|
75
|
+
groups?: ModelOptions['groups'];
|
|
97
76
|
/** Max rows loaded during bootstrap. Only applies to `.instant`. */
|
|
98
77
|
bootstrapLimit?: number;
|
|
99
78
|
/** Bootstrap sort order (e.g. `'created_at DESC'`). */
|
package/dist/schema/sugar.js
CHANGED
|
@@ -46,13 +46,8 @@ function build(shape, opts, baseline) {
|
|
|
46
46
|
lazyObservable: opts?.lazyObservable ?? baseline.lazyObservable,
|
|
47
47
|
typename: opts?.typename,
|
|
48
48
|
tableName: opts?.tableName,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
orgColumn: opts?.orgColumn,
|
|
52
|
-
tenancy: opts?.tenancy,
|
|
53
|
-
scope: opts?.scope,
|
|
54
|
-
grants: opts?.grants,
|
|
55
|
-
entityRoles: opts?.entityRoles,
|
|
49
|
+
policy: opts?.policy,
|
|
50
|
+
groups: opts?.groups,
|
|
56
51
|
bootstrapLimit: opts?.bootstrapLimit,
|
|
57
52
|
bootstrapOrderBy: opts?.bootstrapOrderBy,
|
|
58
53
|
persist: opts?.persist,
|
|
@@ -85,6 +85,7 @@ export declare const deltaAttributionSchema: z.ZodObject<{
|
|
|
85
85
|
system: "system";
|
|
86
86
|
}>>;
|
|
87
87
|
capabilityId: z.ZodNullable<z.ZodString>;
|
|
88
|
+
delegationChainRootUserId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
88
89
|
confirmationState: z.ZodNullable<z.ZodEnum<{
|
|
89
90
|
auto: "auto";
|
|
90
91
|
previewed: "previewed";
|
|
@@ -129,6 +130,7 @@ export declare const syncDeltaRowSchema: z.ZodObject<{
|
|
|
129
130
|
system: "system";
|
|
130
131
|
}>>;
|
|
131
132
|
capabilityId: z.ZodNullable<z.ZodString>;
|
|
133
|
+
delegationChainRootUserId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
132
134
|
confirmationState: z.ZodNullable<z.ZodEnum<{
|
|
133
135
|
auto: "auto";
|
|
134
136
|
previewed: "previewed";
|
|
@@ -67,7 +67,7 @@ export const syncDeltaCoreSchema = z.object({
|
|
|
67
67
|
createdAt: z.string().optional(),
|
|
68
68
|
transactionId: z.string().nullable(),
|
|
69
69
|
});
|
|
70
|
-
// ── Attribution — `control` plane
|
|
70
|
+
// ── Attribution — `control` plane ─────────────────────────────────────────────
|
|
71
71
|
export const deltaAttributionSchema = z.object({
|
|
72
72
|
/** Legacy single-actor column, derived during the dual-write window. */
|
|
73
73
|
createdBy: z.string().nullable(),
|
|
@@ -76,6 +76,7 @@ export const deltaAttributionSchema = z.object({
|
|
|
76
76
|
onBehalfOfId: z.string().nullable(),
|
|
77
77
|
onBehalfOfKind: participantKindSchema.nullable(),
|
|
78
78
|
capabilityId: z.string().nullable(),
|
|
79
|
+
delegationChainRootUserId: z.string().nullable().optional(),
|
|
79
80
|
confirmationState: confirmationStateSchema.nullable(),
|
|
80
81
|
backfillProvenance: backfillProvenanceSchema.nullable(),
|
|
81
82
|
});
|
package/dist/schema/tenancy.d.ts
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tenancy — the single source of truth for how a model's rows are scoped to a
|
|
3
|
-
* tenant.
|
|
4
|
-
*
|
|
5
|
-
* one Zod discriminated union, resolved in one place and consumed everywhere
|
|
6
|
-
* (provision/RLS, introspection, runtime, CLI).
|
|
3
|
+
* tenant. There are exactly two layers here, and keeping them separate is the
|
|
4
|
+
* whole point:
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* 1. The CANONICAL form — {@link Tenancy}, a Zod discriminated union. This is
|
|
7
|
+
* what every consumer (provision/RLS, introspection, runtime, CLI) reads
|
|
8
|
+
* and what crosses the wire in `ModelJSON`. One shape, exhaustively
|
|
9
|
+
* switchable, so the type system holds the isolation boundary.
|
|
10
|
+
* 2. The AUTHORING form — {@link PolicyInput}, the `policy: { by }` option a
|
|
11
|
+
* schema author writes. The name follows Postgres/Supabase RLS vocabulary:
|
|
12
|
+
* a `policy` is the rule that decides which rows a tenant may read.
|
|
13
|
+
* {@link resolvePolicy} maps it to the canonical {@link Tenancy} at
|
|
14
|
+
* `model()`-build time (much as Supabase's `create policy` compiles to a
|
|
15
|
+
* `pg_policy` row), so the authoring vocabulary never reaches the wire or
|
|
16
|
+
* any consumer.
|
|
17
|
+
*
|
|
18
|
+
* Why one authoring option (`policy`) instead of the old
|
|
19
|
+
* `orgScoped`/`scopedVia`/`orgColumn` trio: those three were synonyms for one
|
|
20
|
+
* decision ("how is this row scoped?"), and the most dangerous of them
|
|
21
|
+
* (`orgScoped: false`) silently exposed a whole table cross-tenant. Collapsing
|
|
22
|
+
* them into a single discriminated union makes the opt-out (`{ by: 'none' }`) a
|
|
23
|
+
* loud, deliberate branch instead of a falsy flag — one concept, one name.
|
|
13
24
|
*/
|
|
14
25
|
import { z } from 'zod';
|
|
15
26
|
/** Default physical tenancy column. The ONLY place this literal is canonical. */
|
|
16
27
|
export declare const DEFAULT_ORG_COLUMN = "organization_id";
|
|
17
28
|
/**
|
|
18
29
|
* Scope a table's rows through a parent table (for rows that carry no tenancy
|
|
19
|
-
* column of their own — e.g. `slide_layers` → slide → deck → org).
|
|
30
|
+
* column of their own — e.g. `slide_layers` → slide → deck → org). This is the
|
|
31
|
+
* CANONICAL `parent` payload; authors write the friendlier {@link PolicyInput}
|
|
32
|
+
* `{ by: 'parent', fk, parent }` shape, normalized into this by
|
|
33
|
+
* {@link resolvePolicy}.
|
|
20
34
|
*/
|
|
21
35
|
export declare const scopedViaRefSchema: z.ZodObject<{
|
|
22
36
|
localKey: z.ZodString;
|
|
@@ -25,7 +39,7 @@ export declare const scopedViaRefSchema: z.ZodObject<{
|
|
|
25
39
|
parentOrgColumn: z.ZodOptional<z.ZodString>;
|
|
26
40
|
}, z.core.$strip>;
|
|
27
41
|
export type ScopedViaRef = z.infer<typeof scopedViaRefSchema>;
|
|
28
|
-
/** How a model's rows are scoped to a tenant. */
|
|
42
|
+
/** How a model's rows are scoped to a tenant — the CANONICAL, wire-facing form. */
|
|
29
43
|
export declare const tenancySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
30
44
|
kind: z.ZodLiteral<"column">;
|
|
31
45
|
column: z.ZodString;
|
|
@@ -42,25 +56,50 @@ export declare const tenancySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
42
56
|
}, z.core.$strip>], "kind">;
|
|
43
57
|
export type Tenancy = z.infer<typeof tenancySchema>;
|
|
44
58
|
/**
|
|
45
|
-
*
|
|
46
|
-
* `
|
|
47
|
-
*
|
|
59
|
+
* The AUTHORING form of tenancy — what a schema author writes as the model's
|
|
60
|
+
* `policy` option (Postgres/Supabase RLS vocabulary: a policy is the rule that
|
|
61
|
+
* scopes which rows a tenant may read). A Zod discriminated union on `by`, so
|
|
62
|
+
* the three branches are mutually exclusive and the dangerous opt-out
|
|
63
|
+
* (`{ by: 'none' }`) is an explicit, named choice rather than a falsy flag.
|
|
64
|
+
*
|
|
65
|
+
* - `{ by: 'column' }` — row-local tenancy column (the default).
|
|
66
|
+
* `column` overrides the name (default {@link DEFAULT_ORG_COLUMN}).
|
|
67
|
+
* - `{ by: 'parent', fk, parent }` — inherit tenancy through a foreign key when
|
|
68
|
+
* this table has no tenancy column of its own. `parentKey` (default `'id'`)
|
|
69
|
+
* and `parentTenantColumn` (default {@link DEFAULT_ORG_COLUMN}) are overrides.
|
|
70
|
+
* - `{ by: 'none' }` — genuinely global / reference data. ⚠ Makes
|
|
71
|
+
* the whole table readable cross-tenant — only correct for tenant-less tables.
|
|
48
72
|
*/
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
73
|
+
export declare const policyInputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
74
|
+
by: z.ZodLiteral<"column">;
|
|
75
|
+
column: z.ZodOptional<z.ZodString>;
|
|
76
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
77
|
+
by: z.ZodLiteral<"parent">;
|
|
78
|
+
fk: z.ZodString;
|
|
79
|
+
parent: z.ZodString;
|
|
80
|
+
parentKey: z.ZodOptional<z.ZodString>;
|
|
81
|
+
parentTenantColumn: z.ZodOptional<z.ZodString>;
|
|
82
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
83
|
+
by: z.ZodLiteral<"none">;
|
|
84
|
+
}, z.core.$strip>], "by">;
|
|
85
|
+
export type PolicyInput = z.infer<typeof policyInputSchema>;
|
|
58
86
|
/**
|
|
59
|
-
* Normalize authoring
|
|
60
|
-
* at model
|
|
61
|
-
*
|
|
62
|
-
*
|
|
87
|
+
* Normalize the authoring {@link PolicyInput} into the one canonical
|
|
88
|
+
* {@link Tenancy}. Called once, at `model()`-build, so `ModelDef`/`ModelJSON`
|
|
89
|
+
* and every consumer see only the canonical union. Omitting `policy` defaults
|
|
90
|
+
* to a row-local `organization_id` column.
|
|
63
91
|
*/
|
|
64
|
-
export declare function
|
|
92
|
+
export declare function resolvePolicy(input?: PolicyInput): Tenancy;
|
|
93
|
+
/**
|
|
94
|
+
* Read the canonical {@link Tenancy} off an already-built model def (or parsed
|
|
95
|
+
* `ModelJSON`), defaulting to a row-local `organization_id` column when absent.
|
|
96
|
+
*
|
|
97
|
+
* This is the READ-side helper — consumers (provision/RLS, membership resolver,
|
|
98
|
+
* DDL, CLI) call it to get a model's tenancy without re-deriving the default in
|
|
99
|
+
* each place. It is NOT the authoring normalizer; that's {@link resolveIsolation}.
|
|
100
|
+
*/
|
|
101
|
+
export declare function resolveTenancy(def: {
|
|
102
|
+
tenancy?: Tenancy;
|
|
103
|
+
}): Tenancy;
|
|
65
104
|
/** The physical tenancy column for a column-scoped model, else `null`. */
|
|
66
105
|
export declare function tenancyColumn(t: Tenancy): string | null;
|
package/dist/schema/tenancy.js
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tenancy — the single source of truth for how a model's rows are scoped to a
|
|
3
|
-
* tenant.
|
|
4
|
-
*
|
|
5
|
-
* one Zod discriminated union, resolved in one place and consumed everywhere
|
|
6
|
-
* (provision/RLS, introspection, runtime, CLI).
|
|
3
|
+
* tenant. There are exactly two layers here, and keeping them separate is the
|
|
4
|
+
* whole point:
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* 1. The CANONICAL form — {@link Tenancy}, a Zod discriminated union. This is
|
|
7
|
+
* what every consumer (provision/RLS, introspection, runtime, CLI) reads
|
|
8
|
+
* and what crosses the wire in `ModelJSON`. One shape, exhaustively
|
|
9
|
+
* switchable, so the type system holds the isolation boundary.
|
|
10
|
+
* 2. The AUTHORING form — {@link PolicyInput}, the `policy: { by }` option a
|
|
11
|
+
* schema author writes. The name follows Postgres/Supabase RLS vocabulary:
|
|
12
|
+
* a `policy` is the rule that decides which rows a tenant may read.
|
|
13
|
+
* {@link resolvePolicy} maps it to the canonical {@link Tenancy} at
|
|
14
|
+
* `model()`-build time (much as Supabase's `create policy` compiles to a
|
|
15
|
+
* `pg_policy` row), so the authoring vocabulary never reaches the wire or
|
|
16
|
+
* any consumer.
|
|
17
|
+
*
|
|
18
|
+
* Why one authoring option (`policy`) instead of the old
|
|
19
|
+
* `orgScoped`/`scopedVia`/`orgColumn` trio: those three were synonyms for one
|
|
20
|
+
* decision ("how is this row scoped?"), and the most dangerous of them
|
|
21
|
+
* (`orgScoped: false`) silently exposed a whole table cross-tenant. Collapsing
|
|
22
|
+
* them into a single discriminated union makes the opt-out (`{ by: 'none' }`) a
|
|
23
|
+
* loud, deliberate branch instead of a falsy flag — one concept, one name.
|
|
13
24
|
*/
|
|
14
25
|
import { z } from 'zod';
|
|
15
26
|
/** Default physical tenancy column. The ONLY place this literal is canonical. */
|
|
16
27
|
export const DEFAULT_ORG_COLUMN = 'organization_id';
|
|
17
28
|
/**
|
|
18
29
|
* Scope a table's rows through a parent table (for rows that carry no tenancy
|
|
19
|
-
* column of their own — e.g. `slide_layers` → slide → deck → org).
|
|
30
|
+
* column of their own — e.g. `slide_layers` → slide → deck → org). This is the
|
|
31
|
+
* CANONICAL `parent` payload; authors write the friendlier {@link PolicyInput}
|
|
32
|
+
* `{ by: 'parent', fk, parent }` shape, normalized into this by
|
|
33
|
+
* {@link resolvePolicy}.
|
|
20
34
|
*/
|
|
21
35
|
export const scopedViaRefSchema = z.object({
|
|
22
36
|
/** Column on THIS table pointing at the parent (e.g. `'team_id'`). */
|
|
@@ -28,7 +42,7 @@ export const scopedViaRefSchema = z.object({
|
|
|
28
42
|
/** Column on the parent holding the tenant id. Default {@link DEFAULT_ORG_COLUMN}. */
|
|
29
43
|
parentOrgColumn: z.string().min(1).optional(),
|
|
30
44
|
});
|
|
31
|
-
/** How a model's rows are scoped to a tenant. */
|
|
45
|
+
/** How a model's rows are scoped to a tenant — the CANONICAL, wire-facing form. */
|
|
32
46
|
export const tenancySchema = z.discriminatedUnion('kind', [
|
|
33
47
|
/** Row-local tenancy column (default name `organization_id`, overridable). */
|
|
34
48
|
z.object({ kind: z.literal('column'), column: z.string().min(1) }),
|
|
@@ -38,19 +52,75 @@ export const tenancySchema = z.discriminatedUnion('kind', [
|
|
|
38
52
|
z.object({ kind: z.literal('none') }),
|
|
39
53
|
]);
|
|
40
54
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
55
|
+
* The AUTHORING form of tenancy — what a schema author writes as the model's
|
|
56
|
+
* `policy` option (Postgres/Supabase RLS vocabulary: a policy is the rule that
|
|
57
|
+
* scopes which rows a tenant may read). A Zod discriminated union on `by`, so
|
|
58
|
+
* the three branches are mutually exclusive and the dangerous opt-out
|
|
59
|
+
* (`{ by: 'none' }`) is an explicit, named choice rather than a falsy flag.
|
|
60
|
+
*
|
|
61
|
+
* - `{ by: 'column' }` — row-local tenancy column (the default).
|
|
62
|
+
* `column` overrides the name (default {@link DEFAULT_ORG_COLUMN}).
|
|
63
|
+
* - `{ by: 'parent', fk, parent }` — inherit tenancy through a foreign key when
|
|
64
|
+
* this table has no tenancy column of its own. `parentKey` (default `'id'`)
|
|
65
|
+
* and `parentTenantColumn` (default {@link DEFAULT_ORG_COLUMN}) are overrides.
|
|
66
|
+
* - `{ by: 'none' }` — genuinely global / reference data. ⚠ Makes
|
|
67
|
+
* the whole table readable cross-tenant — only correct for tenant-less tables.
|
|
68
|
+
*/
|
|
69
|
+
export const policyInputSchema = z.discriminatedUnion('by', [
|
|
70
|
+
z.object({
|
|
71
|
+
by: z.literal('column'),
|
|
72
|
+
/** Override the physical tenancy column name. Default {@link DEFAULT_ORG_COLUMN}. */
|
|
73
|
+
column: z.string().min(1).optional(),
|
|
74
|
+
}),
|
|
75
|
+
z.object({
|
|
76
|
+
by: z.literal('parent'),
|
|
77
|
+
/** Column on THIS table pointing at the parent (e.g. `'slideId'`). */
|
|
78
|
+
fk: z.string().min(1),
|
|
79
|
+
/** Parent table name (e.g. `'slides'`). */
|
|
80
|
+
parent: z.string().min(1),
|
|
81
|
+
/** Column on the parent that `fk` references. Default `'id'`. */
|
|
82
|
+
parentKey: z.string().min(1).optional(),
|
|
83
|
+
/** Column on the parent holding the tenant id. Default {@link DEFAULT_ORG_COLUMN}. */
|
|
84
|
+
parentTenantColumn: z.string().min(1).optional(),
|
|
85
|
+
}),
|
|
86
|
+
z.object({ by: z.literal('none') }),
|
|
87
|
+
]);
|
|
88
|
+
/**
|
|
89
|
+
* Normalize the authoring {@link PolicyInput} into the one canonical
|
|
90
|
+
* {@link Tenancy}. Called once, at `model()`-build, so `ModelDef`/`ModelJSON`
|
|
91
|
+
* and every consumer see only the canonical union. Omitting `policy` defaults
|
|
92
|
+
* to a row-local `organization_id` column.
|
|
93
|
+
*/
|
|
94
|
+
export function resolvePolicy(input) {
|
|
95
|
+
if (!input)
|
|
96
|
+
return { kind: 'column', column: DEFAULT_ORG_COLUMN };
|
|
97
|
+
switch (input.by) {
|
|
98
|
+
case 'column':
|
|
99
|
+
return { kind: 'column', column: input.column ?? DEFAULT_ORG_COLUMN };
|
|
100
|
+
case 'parent':
|
|
101
|
+
return {
|
|
102
|
+
kind: 'parent',
|
|
103
|
+
via: {
|
|
104
|
+
localKey: input.fk,
|
|
105
|
+
parentTable: input.parent,
|
|
106
|
+
parentKey: input.parentKey,
|
|
107
|
+
parentOrgColumn: input.parentTenantColumn,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
case 'none':
|
|
111
|
+
return { kind: 'none' };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Read the canonical {@link Tenancy} off an already-built model def (or parsed
|
|
116
|
+
* `ModelJSON`), defaulting to a row-local `organization_id` column when absent.
|
|
117
|
+
*
|
|
118
|
+
* This is the READ-side helper — consumers (provision/RLS, membership resolver,
|
|
119
|
+
* DDL, CLI) call it to get a model's tenancy without re-deriving the default in
|
|
120
|
+
* each place. It is NOT the authoring normalizer; that's {@link resolveIsolation}.
|
|
45
121
|
*/
|
|
46
|
-
export function resolveTenancy(
|
|
47
|
-
|
|
48
|
-
return input.tenancy;
|
|
49
|
-
if (input.scopedVia)
|
|
50
|
-
return { kind: 'parent', via: input.scopedVia };
|
|
51
|
-
if (input.orgScoped === false)
|
|
52
|
-
return { kind: 'none' };
|
|
53
|
-
return { kind: 'column', column: input.orgColumn ?? DEFAULT_ORG_COLUMN };
|
|
122
|
+
export function resolveTenancy(def) {
|
|
123
|
+
return def.tenancy ?? { kind: 'column', column: DEFAULT_ORG_COLUMN };
|
|
54
124
|
}
|
|
55
125
|
/** The physical tenancy column for a column-scoped model, else `null`. */
|
|
56
126
|
export function tenancyColumn(t) {
|
package/dist/server/commit.d.ts
CHANGED
|
@@ -58,11 +58,16 @@ export interface CommitContext {
|
|
|
58
58
|
*/
|
|
59
59
|
onBehalfOf?: ParticipantRef | null;
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* delta so the audit chain "delta → capability → human" is one FK hop.
|
|
61
|
+
* Scoped credential id. Non-null for agent / system commits when the
|
|
62
|
+
* authorizing credential is known; null for human-direct commits.
|
|
64
63
|
*/
|
|
65
64
|
capabilityId?: string | null;
|
|
65
|
+
/**
|
|
66
|
+
* Human user id at the root of the delegated authority chain. Stored directly
|
|
67
|
+
* on `sync_deltas` so audit triggers never need to join mutable credential
|
|
68
|
+
* tables while appending the hash chain.
|
|
69
|
+
*/
|
|
70
|
+
delegationChainRootUserId?: string | null;
|
|
66
71
|
/**
|
|
67
72
|
* ApiKey row id when the caller authenticated with an API key. Used by the
|
|
68
73
|
* idempotency cache and usage attribution. Null for session / capability
|
package/docs/api.md
CHANGED
|
@@ -180,7 +180,7 @@ if (claim) {
|
|
|
180
180
|
|
|
181
181
|
const handle = await ablo.weatherReports.claim({
|
|
182
182
|
id: 'report_stockholm',
|
|
183
|
-
|
|
183
|
+
reason: 'editing',
|
|
184
184
|
ttl: '2m',
|
|
185
185
|
});
|
|
186
186
|
await ablo.weatherReports.update({ id: handle.data.id, data: { status: 'ready' } });
|