@vxil/cli 0.24.2 → 0.25.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/dist/_vxil-feature-configs-apiState.d.ts +77 -2
- package/dist/_vxil-feature-configs-hooks.d.ts +7 -0
- package/dist/_vxil-feature-configs.d.ts +52 -0
- package/dist/config.d.ts +70 -0
- package/dist/config.js +144 -2
- package/dist/vxil-main.js +699 -44
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DeclaredOrgRole, DeclaredRlPolicy, DeclaredWebhookSubscription } from './_vxil-feature-configs.js';
|
|
1
|
+
import type { DeclaredOrgRole, DeclaredRlPolicy, DeclaredSearchCollection, DeclaredWebhookSubscription } from './_vxil-feature-configs.js';
|
|
2
2
|
/** The target_url path markers of the platform's function-delivery lanes
|
|
3
3
|
* (control-plane fnCmsHooks.ts FN_CMSHOOK_PATH / FN_AUTHHOOK_PATH /
|
|
4
4
|
* FN_WEBHOOK_TRIGGER_PATH — mirrored here because this package is
|
|
@@ -6,7 +6,7 @@ import type { DeclaredOrgRole, DeclaredRlPolicy, DeclaredWebhookSubscription } f
|
|
|
6
6
|
* manifest by the deploy reconciler: never declarable, never "undeclared". */
|
|
7
7
|
export declare const FN_TRIGGER_TARGET_MARKERS: readonly ["/v1/internal/fn/cms-hook/", "/v1/internal/fn/auth-hook/", "/v1/internal/fn/trigger/"];
|
|
8
8
|
export declare function isFnTriggerSubscriptionUrl(targetUrl: string): boolean;
|
|
9
|
-
export type ApiStateDatum = 'policies' | 'subscriptions' | 'templates' | 'roles';
|
|
9
|
+
export type ApiStateDatum = 'policies' | 'subscriptions' | 'templates' | 'roles' | 'collections';
|
|
10
10
|
/** feature → the declared-API-state datum its config block may carry. */
|
|
11
11
|
export declare const API_STATE_DATUMS: ReadonlyArray<{
|
|
12
12
|
feature: string;
|
|
@@ -147,6 +147,81 @@ export interface OrgRolePlan {
|
|
|
147
147
|
undeclared: string[];
|
|
148
148
|
}
|
|
149
149
|
export declare function planOrgRoles(declared: readonly DeclaredOrgRole[], live: readonly LiveOrgRole[]): OrgRolePlan;
|
|
150
|
+
/** ONE row of GET /v1/search/collections. `model` / `api_key_ref` are listed by
|
|
151
|
+
* current servers; absent on an older server (then not compared). */
|
|
152
|
+
export interface LiveSearchCollection {
|
|
153
|
+
collection: string;
|
|
154
|
+
dimensions: number;
|
|
155
|
+
provider?: string | null;
|
|
156
|
+
model?: string | null;
|
|
157
|
+
api_key_ref?: string | null;
|
|
158
|
+
documents?: number;
|
|
159
|
+
}
|
|
160
|
+
/** The POST /v1/search/collections body an update sends: always the live
|
|
161
|
+
* dimensions (immutable — an omitted width would fall back to the tenant
|
|
162
|
+
* default and 422). With the provider UNCHANGED every embed field the
|
|
163
|
+
* declaration omits keeps its live value, so a partial declaration never
|
|
164
|
+
* resets the rest to the tenant default (the route is an upsert of the whole
|
|
165
|
+
* embed config). On a PROVIDER CHANGE only the declared fields are sent — the
|
|
166
|
+
* old provider's model / key never carry over to the new one. */
|
|
167
|
+
export interface SearchCollectionBody {
|
|
168
|
+
collection: string;
|
|
169
|
+
dimensions?: number;
|
|
170
|
+
embed?: {
|
|
171
|
+
provider?: string;
|
|
172
|
+
model?: string;
|
|
173
|
+
apiKeyRef?: string;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export interface SearchCollectionPlan {
|
|
177
|
+
create: SearchCollectionBody[];
|
|
178
|
+
/** an embed field the declaration sets differs — POST upserts in place
|
|
179
|
+
* (same collection, documents kept). `reembed`: the provider or model
|
|
180
|
+
* changes on a collection that holds documents (or whose count the server
|
|
181
|
+
* did not list) — the indexed vectors came from the old embedder and new
|
|
182
|
+
* queries would use the new one, so the write is destructive-shaped
|
|
183
|
+
* (pending without --allow-destructive). An apiKeyRef-only change never is. */
|
|
184
|
+
update: Array<{
|
|
185
|
+
body: SearchCollectionBody;
|
|
186
|
+
changed: string[];
|
|
187
|
+
reembed: boolean;
|
|
188
|
+
}>;
|
|
189
|
+
unchanged: string[];
|
|
190
|
+
/** declared `dimensions` differ from the live ones — immutable after
|
|
191
|
+
* create (422 dimensions_immutable), so never written: a refused row */
|
|
192
|
+
immutable: Array<{
|
|
193
|
+
collection: string;
|
|
194
|
+
declared: number;
|
|
195
|
+
live: number;
|
|
196
|
+
}>;
|
|
197
|
+
/** an update the planner refuses to send (a provider change that does not
|
|
198
|
+
* declare the model / apiKeyRef the new provider needs) — a failed row */
|
|
199
|
+
refused: Array<{
|
|
200
|
+
collection: string;
|
|
201
|
+
error: string;
|
|
202
|
+
}>;
|
|
203
|
+
/** live collections the config does not declare — reported, never deleted
|
|
204
|
+
* (there is no collection delete route) */
|
|
205
|
+
undeclared: string[];
|
|
206
|
+
}
|
|
207
|
+
export declare function planSearchCollections(declared: readonly DeclaredSearchCollection[], live: readonly LiveSearchCollection[]): SearchCollectionPlan;
|
|
208
|
+
/** The refused-row error of a declared dimensions change. */
|
|
209
|
+
export declare function searchCollectionImmutableError(i: {
|
|
210
|
+
declared: number;
|
|
211
|
+
live: number;
|
|
212
|
+
}): string;
|
|
213
|
+
/** `vxil pull`: the `vector-search.collections` leaf from the live list —
|
|
214
|
+
* name, dimensions and the pinned embed config, so the pulled file plans
|
|
215
|
+
* with zero drift and a second environment gets the same embedder. Past the
|
|
216
|
+
* declared cap the leaf is NOT written (a pulled file must validate) and
|
|
217
|
+
* `note` says why (the orgs.roles posture). */
|
|
218
|
+
export declare function pulledSearchCollections(rows: readonly Record<string, unknown>[]): {
|
|
219
|
+
collections?: DeclaredSearchCollection[];
|
|
220
|
+
note?: string;
|
|
221
|
+
};
|
|
222
|
+
/** = SEARCH_COLLECTION_DECLARED_CAP (index.ts; not imported — this module is
|
|
223
|
+
* imported BY index.ts). A parity test pins the two. */
|
|
224
|
+
export declare const SEARCH_COLLECTION_PULL_CAP = 50;
|
|
150
225
|
export interface ApiStateSummary {
|
|
151
226
|
datum: ApiStateDatum;
|
|
152
227
|
created: string[];
|
|
@@ -97,6 +97,13 @@ export interface HookCaller {
|
|
|
97
97
|
* caller object still reads
|
|
98
98
|
* as "no roles". */
|
|
99
99
|
roles?: readonly string[] | null;
|
|
100
|
+
/** Workspace scope: the session's ACTIVE org (`caller.org.id` /
|
|
101
|
+
* `caller.org.role`), read-only; null outside active-org mode or with no
|
|
102
|
+
* active org. Optional so an older caller object reads as "no org". */
|
|
103
|
+
org?: {
|
|
104
|
+
id: string;
|
|
105
|
+
role: string | null;
|
|
106
|
+
} | null;
|
|
100
107
|
}
|
|
101
108
|
export interface HookContext {
|
|
102
109
|
item: Record<string, unknown>;
|
|
@@ -323,6 +323,7 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
323
323
|
}>>;
|
|
324
324
|
orgClaims: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
325
325
|
enabled: import("@sinclair/typebox").TBoolean;
|
|
326
|
+
activeOrg: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
326
327
|
}>>;
|
|
327
328
|
security: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
328
329
|
lockout: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
@@ -334,6 +335,7 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
334
335
|
captchaSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
335
336
|
allowedRedirectOrigins: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
336
337
|
inviteOnly: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
338
|
+
verifyLinkEvicts: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
337
339
|
allowedEmailDomains: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
338
340
|
}>>;
|
|
339
341
|
}>;
|
|
@@ -579,6 +581,28 @@ export declare const ActivityFeedConfigSchema: import("@sinclair/typebox").TObje
|
|
|
579
581
|
}>;
|
|
580
582
|
}>;
|
|
581
583
|
export type ActivityFeedConfig = Static<typeof ActivityFeedConfigSchema>;
|
|
584
|
+
/** At most this many declared search collections (the converge's worst-case
|
|
585
|
+
* fan-out: one POST per declared entry per push). */
|
|
586
|
+
export declare const SEARCH_COLLECTION_DECLARED_CAP = 50;
|
|
587
|
+
/** One declared search collection — the POST /v1/search/collections
|
|
588
|
+
* body (vector-search-v1 core.ts CreateCollectionIn), keyed by `collection`.
|
|
589
|
+
* An omitted field is NOT compared against a live collection (it takes the
|
|
590
|
+
* vector-search default on create only); `dimensions` are immutable after
|
|
591
|
+
* create — a declared change is a refused row, never a recreate. */
|
|
592
|
+
/** The widths a search collection accepts — = vector-search-v1 backend.ts
|
|
593
|
+
* ALLOWED_DIMS (one indexed vector column each; a parity test pins them), so
|
|
594
|
+
* an unsupported width fails validation instead of only the push. */
|
|
595
|
+
export declare const SEARCH_COLLECTION_DIMS: readonly [256, 384, 512, 768, 1024, 1536, 3072];
|
|
596
|
+
export declare const DeclaredSearchCollectionSchema: import("@sinclair/typebox").TObject<{
|
|
597
|
+
collection: import("@sinclair/typebox").TString;
|
|
598
|
+
dimensions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<256 | 384 | 512 | 768 | 1024 | 1536 | 3072>[]>>;
|
|
599
|
+
embed: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
600
|
+
provider: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"byov">, import("@sinclair/typebox").TLiteral<"mock">, import("@sinclair/typebox").TLiteral<"openai">, import("@sinclair/typebox").TLiteral<"cohere">]>>;
|
|
601
|
+
model: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
602
|
+
apiKeyRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
603
|
+
}>>;
|
|
604
|
+
}>;
|
|
605
|
+
export type DeclaredSearchCollection = Static<typeof DeclaredSearchCollectionSchema>;
|
|
582
606
|
export declare const VectorSearchConfigSchema: import("@sinclair/typebox").TObject<{
|
|
583
607
|
enabled: import("@sinclair/typebox").TBoolean;
|
|
584
608
|
backend: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"auto">, import("@sinclair/typebox").TLiteral<"lakebase">, import("@sinclair/typebox").TLiteral<"pgvector">]>;
|
|
@@ -616,6 +640,15 @@ export declare const VectorSearchConfigSchema: import("@sinclair/typebox").TObje
|
|
|
616
640
|
statusFilter: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"published">, import("@sinclair/typebox").TLiteral<"all">]>>;
|
|
617
641
|
cron: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
618
642
|
}>>>;
|
|
643
|
+
collections: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
644
|
+
collection: import("@sinclair/typebox").TString;
|
|
645
|
+
dimensions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<256 | 384 | 512 | 768 | 1024 | 1536 | 3072>[]>>;
|
|
646
|
+
embed: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
647
|
+
provider: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"byov">, import("@sinclair/typebox").TLiteral<"mock">, import("@sinclair/typebox").TLiteral<"openai">, import("@sinclair/typebox").TLiteral<"cohere">]>>;
|
|
648
|
+
model: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
649
|
+
apiKeyRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
650
|
+
}>>;
|
|
651
|
+
}>>>;
|
|
619
652
|
}>;
|
|
620
653
|
export type VectorSearchConfig = Static<typeof VectorSearchConfigSchema>;
|
|
621
654
|
export declare const AI_TEMPLATE_NAME_PATTERN = "^[a-zA-Z0-9_.\\-]+$";
|
|
@@ -766,6 +799,12 @@ export declare const FunctionsConfigSchema: import("@sinclair/typebox").TObject<
|
|
|
766
799
|
metering: import("@sinclair/typebox").TObject<{
|
|
767
800
|
enabled: import("@sinclair/typebox").TBoolean;
|
|
768
801
|
}>;
|
|
802
|
+
logDrain: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
803
|
+
url: import("@sinclair/typebox").TString;
|
|
804
|
+
secretRef: import("@sinclair/typebox").TString;
|
|
805
|
+
minLevel: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"debug">, import("@sinclair/typebox").TLiteral<"info">, import("@sinclair/typebox").TLiteral<"log">, import("@sinclair/typebox").TLiteral<"warn">, import("@sinclair/typebox").TLiteral<"error">]>>;
|
|
806
|
+
dailyLines: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
|
|
807
|
+
}>>;
|
|
769
808
|
functions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TObject<{
|
|
770
809
|
bindings: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
771
810
|
kind: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"http">, import("@sinclair/typebox").TLiteral<"queue">, import("@sinclair/typebox").TLiteral<"cron">, import("@sinclair/typebox").TLiteral<"webhook">, import("@sinclair/typebox").TLiteral<"cmsHook">, import("@sinclair/typebox").TLiteral<"authHook">]>;
|
|
@@ -920,6 +959,7 @@ export declare function fnServerScopesError(raw: unknown, fnScopes?: readonly st
|
|
|
920
959
|
* templateVars parity). */
|
|
921
960
|
export declare function rlTemplateVars(template: string): string[];
|
|
922
961
|
export declare function validateDeclaredRlPolicies(policies: readonly DeclaredRlPolicy[] | undefined): string[];
|
|
962
|
+
export declare function validateDeclaredSearchCollections(collections: readonly DeclaredSearchCollection[] | undefined): string[];
|
|
923
963
|
export declare function validateDeclaredOrgRoles(roles: readonly DeclaredOrgRole[] | undefined): string[];
|
|
924
964
|
export declare function validateDeclaredWebhookSubscriptions(subscriptions: readonly DeclaredWebhookSubscription[] | undefined, maxSubscriptions: number | undefined): string[];
|
|
925
965
|
/** The stored-template `schema` bounds — MIRRORED from @vxil/runtime
|
|
@@ -946,3 +986,15 @@ export interface PlannerHint {
|
|
|
946
986
|
dependsOn: FeatureKey[];
|
|
947
987
|
}
|
|
948
988
|
export declare const FEATURE_HINTS: Record<FeatureKey, PlannerHint>;
|
|
989
|
+
/** = cms-v1 core.ts RETAIN_DAYS_MAX (a parity test pins the two). */
|
|
990
|
+
export declare const CMS_RETAIN_DAYS_MAX = 3650;
|
|
991
|
+
/** A declared `retainDays`: absent = NOT MANAGED (the live value is left as
|
|
992
|
+
* is — a collection whose retention was set by API is never silently
|
|
993
|
+
* cleared by a push that does not mention it); `null` = keep items until
|
|
994
|
+
* deleted; an integer 1..3650 = days. */
|
|
995
|
+
export declare function isDeclaredRetainDays(v: unknown): v is number | null | undefined;
|
|
996
|
+
/** Does moving retention from `remote` to `declared` make the sweep delete
|
|
997
|
+
* MORE items (turning retention on, or shortening it)? On an EXISTING
|
|
998
|
+
* collection that change applies only under allow-destructive; lengthening
|
|
999
|
+
* or clearing keeps more and always applies. */
|
|
1000
|
+
export declare function retainDaysShortens(declared: number | null, remote: number | null): boolean;
|
package/dist/config.d.ts
CHANGED
|
@@ -132,13 +132,83 @@ export interface CollectionDef {
|
|
|
132
132
|
* a config leaf) and carried by BOTH push paths; widening (clearing, or
|
|
133
133
|
* adding a role) needs `vxil push --allow-destructive`. */
|
|
134
134
|
writeRoles?: string[];
|
|
135
|
+
/** Retention in days (guide ch. 4): items created more than this many days
|
|
136
|
+
* ago are soft-deleted by the nightly sweep (recoverable for 30 days). An
|
|
137
|
+
* integer 1..3650, or `null` to keep items until deleted. ABSENT = not
|
|
138
|
+
* managed by config — a value set by API or in the dashboard is left as is.
|
|
139
|
+
* Set at create; on an existing collection, turning retention on or
|
|
140
|
+
* shortening it needs `vxil push --allow-destructive` (lengthening or
|
|
141
|
+
* `null` always applies). Carried by BOTH push paths. */
|
|
142
|
+
retainDays?: number | null;
|
|
135
143
|
/** Per-record action buttons (guide ch. 4): `[{ key, label, fn }]`, stored on
|
|
136
144
|
* the collection (model data, not a config leaf). The dashboard renders one
|
|
137
145
|
* button per action on each record row; `POST /v1/cms/items/:coll/:id/actions/
|
|
138
146
|
* :key` invokes `fn` once. Carried by BOTH push paths (create + reconcile on
|
|
139
147
|
* existing collections — config is the source of truth; `[]`/absent clears). */
|
|
140
148
|
actions?: CmsActionDef[];
|
|
149
|
+
/** WORKSPACE SCOPING (guide ch. 4 / 9): names an existing `string` field with
|
|
150
|
+
* an `indexSlot` in `s1`–`s4` that holds the row's workspace (an orgs
|
|
151
|
+
* `org_id`). A VERIFIED end user then reads and writes only the rows of the
|
|
152
|
+
* org their session is ACTIVE in; server keys are unaffected. Needs
|
|
153
|
+
* `features.auth.orgClaims: { enabled: true, activeOrg: true }`. May coexist
|
|
154
|
+
* with `ownerField` (both predicates apply). Not together with a `cdc` rule or
|
|
155
|
+
* a vector-search `sync[]` source in this release. Carried by BOTH push paths
|
|
156
|
+
* (wire `org_field`); clearing it WIDENS access and needs
|
|
157
|
+
* `vxil push --allow-destructive`. */
|
|
158
|
+
orgField?: string;
|
|
159
|
+
/** The ACTIVE-ORG role slugs one of which an end user must hold to READ this
|
|
160
|
+
* `orgField` collection (absent = any member of the workspace). Up to 16 slugs
|
|
161
|
+
* (`^[a-z0-9][a-z0-9_-]{0,31}$`); refused without `orgField`. Carried by BOTH
|
|
162
|
+
* push paths (wire `org_read_roles`); clearing it, or adding a role, WIDENS
|
|
163
|
+
* access and needs `vxil push --allow-destructive`. */
|
|
164
|
+
orgReadRoles?: string[];
|
|
141
165
|
}
|
|
166
|
+
/** The cms role-slug alphabet (workers/cms-v1/src/readRoles.ts READ_ROLE_RE):
|
|
167
|
+
* gate slugs are at most 32 characters. */
|
|
168
|
+
export declare const CMS_ROLE_SLUG_RE: RegExp;
|
|
169
|
+
/** At most this many `orgReadRoles` slugs per collection (cms/0195 CHECK). */
|
|
170
|
+
export declare const CMS_ORG_READ_ROLES_MAX = 16;
|
|
171
|
+
/** A collection's declared orgField in either spelling (config `orgField`, wire
|
|
172
|
+
* `org_field` — the /v1/apply bundle accepts both); a non-empty string or null. */
|
|
173
|
+
export declare function declaredOrgField(def: unknown): string | null;
|
|
174
|
+
/** Collection-local WORKSPACE rules — the same refusals cms-v1 makes on the
|
|
175
|
+
* collection upsert, so a config fails at `defineConfig` / `collection()` /
|
|
176
|
+
* `/v1/apply` instead of mid-push. Accepts both spellings (`orgField` /
|
|
177
|
+
* `org_field`, `orgReadRoles` / `org_read_roles`, `indexSlot` / `index_slot`).
|
|
178
|
+
* Returns every violation (empty = valid). */
|
|
179
|
+
export declare function workspaceCollectionErrors(name: string, def: unknown): string[];
|
|
180
|
+
/** Throwing form of `workspaceCollectionErrors` (first violation). */
|
|
181
|
+
export declare function assertWorkspaceCollection(name: string, def: unknown): void;
|
|
182
|
+
/** The manifest shape `workspaceManifestErrors` reads: a `VxilConfig` or a
|
|
183
|
+
* `/v1/apply` bundle (features keyed by feature id, cms collections in either
|
|
184
|
+
* spelling). */
|
|
185
|
+
export interface WorkspaceManifestInput {
|
|
186
|
+
features?: Record<string, unknown> | VxilConfig['features'];
|
|
187
|
+
cms?: {
|
|
188
|
+
collections?: Record<string, unknown>;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** CROSS-FEATURE workspace rules — the ones only the WHOLE manifest can see
|
|
192
|
+
* (the cms reconciler sees one collection). Run by `defineConfig` (so every
|
|
193
|
+
* CLI push evaluates it) and by control-plane `/v1/apply` (the server-side
|
|
194
|
+
* authority). Returns every violation (empty = valid):
|
|
195
|
+
* - orgField needs `features.auth.orgClaims.enabled` AND `.activeOrg`;
|
|
196
|
+
* - no `features.cms.cdc` rule on an orgField collection (Phase 1);
|
|
197
|
+
* - no vector-search `sync[]` source reading an orgField collection (Phase 1);
|
|
198
|
+
* - a read model materializing an orgField source must target an orgField
|
|
199
|
+
* collection whose orgField has the SAME name, and group (or partition) by
|
|
200
|
+
* the source orgField — so each rollup row lands in its own workspace;
|
|
201
|
+
* - an `orgs.roles` key over 32 characters cannot be named in a gate
|
|
202
|
+
* (orgReadRoles / writeRoles / field readRoles) of an orgField collection. */
|
|
203
|
+
export declare function workspaceManifestErrors(config: WorkspaceManifestInput,
|
|
204
|
+
/** `checkAuth: false` skips the activeOrg rule — `/v1/apply` passes it when
|
|
205
|
+
* the bundle neither declares an orgField collection nor features.auth, so
|
|
206
|
+
* pre-existing live state alone never blocks an unrelated apply. */
|
|
207
|
+
opts?: {
|
|
208
|
+
checkAuth?: boolean;
|
|
209
|
+
}): string[];
|
|
210
|
+
/** Throwing form of `workspaceManifestErrors` — run by `defineConfig`. */
|
|
211
|
+
export declare function assertWorkspaceManifest(config: WorkspaceManifestInput): void;
|
|
142
212
|
/** The auth lifecycle events an `authHook` binding may subscribe to
|
|
143
213
|
* (2026-09-10) — a CLOSED union; ONE event per binding, one audit
|
|
144
214
|
* event → one function invocation, no branching inside vxil:
|
package/dist/config.js
CHANGED
|
@@ -16,6 +16,136 @@ function assertIndexedFields(name, def) {
|
|
|
16
16
|
throw new Error(`vxil.config: cms.${name} has ${n} indexed fields \u2014 at most ${CMS_INDEXED_FIELD_BUDGET} per collection`);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
var CMS_ROLE_SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
20
|
+
var CMS_ORG_READ_ROLES_MAX = 16;
|
|
21
|
+
var ORG_FIELD_SLOTS = /* @__PURE__ */ new Set(["s1", "s2", "s3", "s4"]);
|
|
22
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
23
|
+
function declaredOrgField(def) {
|
|
24
|
+
if (!isObj(def)) return null;
|
|
25
|
+
const v = def.orgField ?? def.org_field;
|
|
26
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
27
|
+
}
|
|
28
|
+
function workspaceCollectionErrors(name, def) {
|
|
29
|
+
if (!isObj(def)) return [];
|
|
30
|
+
const errs = [];
|
|
31
|
+
const at = `cms.collections.${name}`;
|
|
32
|
+
const camel = def.orgField;
|
|
33
|
+
const snake = def.org_field;
|
|
34
|
+
if (camel !== void 0 && snake !== void 0 && camel !== snake) {
|
|
35
|
+
errs.push(`${at} sets orgField and org_field to different values \u2014 send one`);
|
|
36
|
+
}
|
|
37
|
+
const rawOrg = camel ?? snake;
|
|
38
|
+
let orgField = null;
|
|
39
|
+
if (rawOrg !== void 0 && rawOrg !== null) {
|
|
40
|
+
if (typeof rawOrg !== "string" || rawOrg.length === 0) {
|
|
41
|
+
errs.push(`${at}.orgField must be a field name (got ${JSON.stringify(rawOrg)})`);
|
|
42
|
+
} else {
|
|
43
|
+
orgField = rawOrg;
|
|
44
|
+
const f = isObj(def.fields) ? def.fields[rawOrg] : void 0;
|
|
45
|
+
if (!isObj(f)) {
|
|
46
|
+
errs.push(`${at}.orgField '${rawOrg}' names no declared field`);
|
|
47
|
+
} else {
|
|
48
|
+
if (f.type !== "string") {
|
|
49
|
+
errs.push(`${at}.orgField '${rawOrg}' must be a string field (is ${JSON.stringify(f.type)})`);
|
|
50
|
+
}
|
|
51
|
+
const slot = f.indexSlot ?? f.index_slot;
|
|
52
|
+
if (typeof slot !== "string" || !ORG_FIELD_SLOTS.has(slot)) {
|
|
53
|
+
errs.push(`${at}.orgField '${rawOrg}' needs an indexSlot in s1\u2013s4 (every end-user read filters on it)`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const owner = def.ownerField ?? def.owner_field;
|
|
59
|
+
if (orgField !== null && owner === orgField) {
|
|
60
|
+
errs.push(`${at}.orgField and ownerField must name different fields (both are '${orgField}')`);
|
|
61
|
+
}
|
|
62
|
+
const rrCamel = def.orgReadRoles;
|
|
63
|
+
const rrSnake = def.org_read_roles;
|
|
64
|
+
if (rrCamel !== void 0 && rrSnake !== void 0 && JSON.stringify(rrCamel) !== JSON.stringify(rrSnake)) {
|
|
65
|
+
errs.push(`${at} sets orgReadRoles and org_read_roles to different values \u2014 send one`);
|
|
66
|
+
}
|
|
67
|
+
const rr = rrCamel ?? rrSnake;
|
|
68
|
+
if (rr !== void 0 && rr !== null) {
|
|
69
|
+
if (!Array.isArray(rr) || rr.some((r) => typeof r !== "string" || !CMS_ROLE_SLUG_RE.test(r))) {
|
|
70
|
+
errs.push(`${at}.orgReadRoles must be an array of role slugs matching ${CMS_ROLE_SLUG_RE} (got ${JSON.stringify(rr)})`);
|
|
71
|
+
} else if (rr.length > CMS_ORG_READ_ROLES_MAX) {
|
|
72
|
+
errs.push(`${at}.orgReadRoles has ${rr.length} entries \u2014 at most ${CMS_ORG_READ_ROLES_MAX}`);
|
|
73
|
+
} else if (rr.length > 0 && orgField === null) {
|
|
74
|
+
errs.push(`${at}.orgReadRoles needs orgField (it gates reads of a workspace-scoped collection)`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return errs;
|
|
78
|
+
}
|
|
79
|
+
function assertWorkspaceCollection(name, def) {
|
|
80
|
+
const errs = workspaceCollectionErrors(name, def);
|
|
81
|
+
if (errs.length) throw new Error(`vxil.config: ${errs[0]}`);
|
|
82
|
+
}
|
|
83
|
+
var asList = (v) => Array.isArray(v) ? v : v === void 0 || v === null ? [] : [v];
|
|
84
|
+
var stringsOf = (v) => asList(v).filter((s) => typeof s === "string");
|
|
85
|
+
function workspaceManifestErrors(config, opts = {}) {
|
|
86
|
+
const errs = [];
|
|
87
|
+
const rawCollections = config.cms?.collections;
|
|
88
|
+
const collections = isObj(rawCollections) ? rawCollections : {};
|
|
89
|
+
const orgCols = /* @__PURE__ */ new Map();
|
|
90
|
+
for (const [name, def] of Object.entries(collections)) {
|
|
91
|
+
const of = declaredOrgField(def);
|
|
92
|
+
if (of) orgCols.set(name, of);
|
|
93
|
+
}
|
|
94
|
+
if (orgCols.size === 0) return errs;
|
|
95
|
+
const features = isObj(config.features) ? config.features : {};
|
|
96
|
+
const names = [...orgCols.keys()].join(", ");
|
|
97
|
+
const auth = isObj(features.auth) ? features.auth : {};
|
|
98
|
+
const oc = isObj(auth.orgClaims) ? auth.orgClaims : {};
|
|
99
|
+
if (opts.checkAuth !== false && (oc.enabled !== true || oc.activeOrg !== true)) {
|
|
100
|
+
errs.push(`cms.collections ${names} set orgField, which needs features.auth.orgClaims { enabled: true, activeOrg: true } \u2014 without an active org the session carries no stable workspace`);
|
|
101
|
+
}
|
|
102
|
+
const cms = isObj(features.cms) ? features.cms : {};
|
|
103
|
+
for (const [rule, r] of Object.entries(isObj(cms.cdc) ? cms.cdc : {})) {
|
|
104
|
+
if (isObj(r) && typeof r.collection === "string" && orgCols.has(r.collection)) {
|
|
105
|
+
errs.push(`features.cms.cdc.${rule}: collection '${r.collection}' is workspace-scoped (orgField) \u2014 a cdc channel is not access control, so it is refused until org channels ship (Phase 2); fan out from a function with per-workspace channel names meanwhile`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const vs = isObj(features["vector-search"]) ? features["vector-search"] : {};
|
|
109
|
+
asList(vs.sync).forEach((s, i) => {
|
|
110
|
+
if (isObj(s) && typeof s.cmsCollection === "string" && orgCols.has(s.cmsCollection)) {
|
|
111
|
+
errs.push(`features.vector-search.sync[${i}]: cmsCollection '${s.cmsCollection}' is workspace-scoped (orgField) \u2014 workspace-scoped sync arrives with orgScoped (Phase 2); search through a function meanwhile`);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
for (const [rmName, rm] of Object.entries(isObj(cms.readModels) ? cms.readModels : {})) {
|
|
115
|
+
if (!isObj(rm) || typeof rm.collection !== "string") continue;
|
|
116
|
+
const srcOrg = orgCols.get(rm.collection);
|
|
117
|
+
const to = isObj(rm.materialize) ? rm.materialize.to : void 0;
|
|
118
|
+
if (!srcOrg || typeof to !== "string") continue;
|
|
119
|
+
const tOrg = orgCols.get(to);
|
|
120
|
+
const spec = isObj(rm.spec) ? rm.spec : {};
|
|
121
|
+
const groups = [...stringsOf(spec.groupBy), ...stringsOf(spec.partitionBy)];
|
|
122
|
+
if (tOrg !== srcOrg || !groups.includes(srcOrg)) {
|
|
123
|
+
errs.push(`features.cms.readModels.${rmName}: materializes workspace-scoped '${rm.collection}' into '${to}' \u2014 the target must be an orgField collection with orgField '${srcOrg}' and the read model must group by '${srcOrg}', or one workspace's rollup rows land where every end user can read them`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const orgs = isObj(features.orgs) ? features.orgs : {};
|
|
127
|
+
const longRoles = new Set(asList(orgs.roles).map((r) => isObj(r) ? r.role_key : void 0).filter((k) => typeof k === "string" && k.length > 32));
|
|
128
|
+
for (const [name] of orgCols) {
|
|
129
|
+
const def = collections[name];
|
|
130
|
+
const gate = /* @__PURE__ */ new Set([
|
|
131
|
+
...stringsOf(def.orgReadRoles ?? def.org_read_roles),
|
|
132
|
+
...stringsOf(def.writeRoles ?? def.write_roles)
|
|
133
|
+
]);
|
|
134
|
+
for (const f of Object.values(isObj(def.fields) ? def.fields : {})) {
|
|
135
|
+
if (isObj(f)) for (const r of stringsOf(f.readRoles ?? f.read_roles)) gate.add(r);
|
|
136
|
+
}
|
|
137
|
+
for (const r of gate) {
|
|
138
|
+
if (r.length > 32 || longRoles.has(r)) {
|
|
139
|
+
errs.push(`cms.collections.${name}: gate role '${r}' is longer than 32 characters \u2014 an orgs role named in a cms gate must be 32 characters or fewer (longer keys stay legal in orgs)`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return errs;
|
|
144
|
+
}
|
|
145
|
+
function assertWorkspaceManifest(config) {
|
|
146
|
+
const errs = workspaceManifestErrors(config);
|
|
147
|
+
if (errs.length) throw new Error(`vxil.config: ${errs.join("; ")}`);
|
|
148
|
+
}
|
|
19
149
|
var RELEASED_API_VERSIONS = ["v1"];
|
|
20
150
|
function defineConfig(c) {
|
|
21
151
|
if (c.apiVersion !== void 0 && !RELEASED_API_VERSIONS.includes(c.apiVersion)) {
|
|
@@ -23,7 +153,11 @@ function defineConfig(c) {
|
|
|
23
153
|
`vxil.config: apiVersion '${String(c.apiVersion)}' is not a released API major (released: ${RELEASED_API_VERSIONS.map((v) => `'${v}'`).join(", ")}). A new major ships as a new path with its own deprecation window \u2014 see https://vxil.com/docs/guide/12-going-to-production.`
|
|
24
154
|
);
|
|
25
155
|
}
|
|
26
|
-
for (const [name, def] of Object.entries(c.cms?.collections ?? {}))
|
|
156
|
+
for (const [name, def] of Object.entries(c.cms?.collections ?? {})) {
|
|
157
|
+
assertIndexedFields(name, def);
|
|
158
|
+
assertWorkspaceCollection(name, def);
|
|
159
|
+
}
|
|
160
|
+
assertWorkspaceManifest(c);
|
|
27
161
|
return c;
|
|
28
162
|
}
|
|
29
163
|
function field(type, opts = {}) {
|
|
@@ -31,13 +165,21 @@ function field(type, opts = {}) {
|
|
|
31
165
|
}
|
|
32
166
|
function collection(def) {
|
|
33
167
|
assertIndexedFields(def.singular ?? "collection", def);
|
|
168
|
+
assertWorkspaceCollection(def.singular ?? "collection", def);
|
|
34
169
|
return def;
|
|
35
170
|
}
|
|
36
171
|
export {
|
|
37
172
|
CMS_INDEXED_FIELD_BUDGET,
|
|
173
|
+
CMS_ORG_READ_ROLES_MAX,
|
|
174
|
+
CMS_ROLE_SLUG_RE,
|
|
38
175
|
RELEASED_API_VERSIONS,
|
|
39
176
|
assertIndexedFields,
|
|
177
|
+
assertWorkspaceCollection,
|
|
178
|
+
assertWorkspaceManifest,
|
|
40
179
|
collection,
|
|
180
|
+
declaredOrgField,
|
|
41
181
|
defineConfig,
|
|
42
|
-
field
|
|
182
|
+
field,
|
|
183
|
+
workspaceCollectionErrors,
|
|
184
|
+
workspaceManifestErrors
|
|
43
185
|
};
|