@vxil/cli 0.24.2 → 0.26.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 +97 -2
- package/dist/_vxil-feature-configs-hooks.d.ts +7 -0
- package/dist/_vxil-feature-configs-readmodels.d.ts +35 -2
- package/dist/_vxil-feature-configs.d.ts +61 -0
- package/dist/config.d.ts +77 -0
- package/dist/config.js +174 -2
- package/dist/vxil-main.js +956 -73
- 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,90 @@ 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
|
+
/** 'arabic' | 'none'; absent on an older server (then not compared). */
|
|
159
|
+
keyword_normalize?: string | null;
|
|
160
|
+
documents?: number;
|
|
161
|
+
/** WORKSPACE-ROWS Phase 2: listed (true) only on an orgScoped collection */
|
|
162
|
+
org_scoped?: boolean;
|
|
163
|
+
}
|
|
164
|
+
/** The POST /v1/search/collections body an update sends: always the live
|
|
165
|
+
* dimensions (immutable — an omitted width would fall back to the tenant
|
|
166
|
+
* default and 422). With the provider UNCHANGED every embed field the
|
|
167
|
+
* declaration omits keeps its live value, so a partial declaration never
|
|
168
|
+
* resets the rest to the tenant default (the route is an upsert of the whole
|
|
169
|
+
* embed config). On a PROVIDER CHANGE only the declared fields are sent — the
|
|
170
|
+
* old provider's model / key never carry over to the new one. */
|
|
171
|
+
export interface SearchCollectionBody {
|
|
172
|
+
collection: string;
|
|
173
|
+
dimensions?: number;
|
|
174
|
+
embed?: {
|
|
175
|
+
provider?: string;
|
|
176
|
+
model?: string;
|
|
177
|
+
apiKeyRef?: string;
|
|
178
|
+
};
|
|
179
|
+
/** sent only when declared (the route keeps the live fold when omitted) */
|
|
180
|
+
keywordNormalize?: 'arabic' | 'none';
|
|
181
|
+
/** create only — immutable after create (an update never sends it, so the
|
|
182
|
+
* server keeps the live value) */
|
|
183
|
+
org_scoped?: boolean;
|
|
184
|
+
}
|
|
185
|
+
export interface SearchCollectionPlan {
|
|
186
|
+
create: SearchCollectionBody[];
|
|
187
|
+
/** an embed field the declaration sets differs — POST upserts in place
|
|
188
|
+
* (same collection, documents kept). `reembed`: the provider or model
|
|
189
|
+
* changes on a collection that holds documents (or whose count the server
|
|
190
|
+
* did not list) — the indexed vectors came from the old embedder and new
|
|
191
|
+
* queries would use the new one, so the write is destructive-shaped
|
|
192
|
+
* (pending without --allow-destructive). An apiKeyRef-only change never is. */
|
|
193
|
+
update: Array<{
|
|
194
|
+
body: SearchCollectionBody;
|
|
195
|
+
changed: string[];
|
|
196
|
+
reembed: boolean;
|
|
197
|
+
}>;
|
|
198
|
+
unchanged: string[];
|
|
199
|
+
/** declared `dimensions` differ from the live ones — immutable after
|
|
200
|
+
* create (422 dimensions_immutable), so never written: a refused row */
|
|
201
|
+
immutable: Array<{
|
|
202
|
+
collection: string;
|
|
203
|
+
declared: number;
|
|
204
|
+
live: number;
|
|
205
|
+
}>;
|
|
206
|
+
/** an update the planner refuses to send (a provider change that does not
|
|
207
|
+
* declare the model / apiKeyRef the new provider needs) — a failed row */
|
|
208
|
+
refused: Array<{
|
|
209
|
+
collection: string;
|
|
210
|
+
error: string;
|
|
211
|
+
}>;
|
|
212
|
+
/** live collections the config does not declare — reported, never deleted
|
|
213
|
+
* (there is no collection delete route) */
|
|
214
|
+
undeclared: string[];
|
|
215
|
+
}
|
|
216
|
+
export declare function planSearchCollections(declared: readonly DeclaredSearchCollection[], live: readonly LiveSearchCollection[]): SearchCollectionPlan;
|
|
217
|
+
/** The refused-row error of a declared dimensions change. */
|
|
218
|
+
export declare function searchCollectionImmutableError(i: {
|
|
219
|
+
declared: number;
|
|
220
|
+
live: number;
|
|
221
|
+
}): string;
|
|
222
|
+
/** `vxil pull`: the `vector-search.collections` leaf from the live list —
|
|
223
|
+
* name, dimensions and the pinned embed config, so the pulled file plans
|
|
224
|
+
* with zero drift and a second environment gets the same embedder. Past the
|
|
225
|
+
* declared cap the leaf is NOT written (a pulled file must validate) and
|
|
226
|
+
* `note` says why (the orgs.roles posture). */
|
|
227
|
+
export declare function pulledSearchCollections(rows: readonly Record<string, unknown>[]): {
|
|
228
|
+
collections?: DeclaredSearchCollection[];
|
|
229
|
+
note?: string;
|
|
230
|
+
};
|
|
231
|
+
/** = SEARCH_COLLECTION_DECLARED_CAP (index.ts; not imported — this module is
|
|
232
|
+
* imported BY index.ts). A parity test pins the two. */
|
|
233
|
+
export declare const SEARCH_COLLECTION_PULL_CAP = 50;
|
|
150
234
|
export interface ApiStateSummary {
|
|
151
235
|
datum: ApiStateDatum;
|
|
152
236
|
created: string[];
|
|
@@ -168,7 +252,18 @@ export interface ApiStateSummary {
|
|
|
168
252
|
key: string;
|
|
169
253
|
error: string;
|
|
170
254
|
}>;
|
|
255
|
+
/** follow-up the operator must act on for a row that DID converge (e.g. a
|
|
256
|
+
* search collection whose keyword fold was turned on needs a re-ingest);
|
|
257
|
+
* absent = none (older servers never send it) */
|
|
258
|
+
notes?: Array<{
|
|
259
|
+
key: string;
|
|
260
|
+
note: string;
|
|
261
|
+
}>;
|
|
171
262
|
}
|
|
263
|
+
/** The note a converge reports when it turned the keyword fold ON for a
|
|
264
|
+
* search collection that already holds documents (the server answered
|
|
265
|
+
* keyword_reindex_required: true). */
|
|
266
|
+
export declare const SEARCH_KEYWORD_REINDEX_NOTE = "keyword fold turned on \u2014 chunks indexed before it match only their exact spelling until re-ingested (re-ingest, or reset the sync source)";
|
|
172
267
|
export declare function emptyApiStateSummary(datum: ApiStateDatum): ApiStateSummary;
|
|
173
268
|
/** The `api_state` object a config-write response carries (`PUT /v1/config/
|
|
174
269
|
* :feature` and its dashboard twin, dry-run included — control-plane
|
|
@@ -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>;
|
|
@@ -33,16 +33,37 @@ interface CdcRuleLike {
|
|
|
33
33
|
payload?: unknown;
|
|
34
34
|
enabled?: boolean;
|
|
35
35
|
}
|
|
36
|
+
/** The template token: one WHOLE colon-separated segment of a cdc `channel`. */
|
|
37
|
+
export declare const CDC_ORG_TOKEN = "{org}";
|
|
38
|
+
/** Every resolved workspace channel starts with this prefix; realtime-v1 binds
|
|
39
|
+
* an end-user connect token for such a channel to the verified `org_id` claim
|
|
40
|
+
* (its own copy: `workers/realtime-v1/src/token.ts` ORG_CHANNEL_PREFIX; the
|
|
41
|
+
* `cdc-gated-fields` gate pins both copies equal). */
|
|
42
|
+
export declare const ORG_CHANNEL_PREFIX = "org:";
|
|
43
|
+
/** An org id that may enter a channel name: no ':' (it would shift the segment
|
|
44
|
+
* realtime compares), the realtime channel alphabet, ≤ 64 chars (orgs ids are
|
|
45
|
+
* `org_…`, at most 64 chars). */
|
|
46
|
+
export declare const ORG_CHANNEL_ID_RE: RegExp;
|
|
47
|
+
/** Is `channel` an org template — exactly one segment equal to `{org}`, every
|
|
48
|
+
* other segment a non-empty channel-alphabet run, ≤ 128 chars in all? */
|
|
49
|
+
export declare function isOrgChannelTemplate(channel: string): boolean;
|
|
50
|
+
/** Resolve a template for one row's workspace: drop the `{org}` segment and
|
|
51
|
+
* prefix `org:<id>:` (`notes:{org}` → `org:<id>:notes`; a bare `{org}` →
|
|
52
|
+
* `org:<id>`). Null (publish nothing) for a non-template or an org id that is
|
|
53
|
+
* not a string matching ORG_CHANNEL_ID_RE — fail closed. */
|
|
54
|
+
export declare function resolveOrgChannel(template: string, orgId: unknown): string | null;
|
|
36
55
|
/** Cross-field rule for CmsConfig.cdc (called from validateFeatureConfig). */
|
|
37
56
|
export declare function validateCdcConfig(cdc: Record<string, CdcRuleLike> | undefined): string[];
|
|
38
57
|
/** What a payload:'full' change-data rule must know about its target
|
|
39
58
|
* collection's read gates. `readRoleFields`: fields with a non-empty
|
|
40
59
|
* `readRoles`; `endUserAccess`: the collection's end-user access mode;
|
|
41
|
-
* `ownerField`: the end-user owner-scope field (null/absent = none)
|
|
60
|
+
* `ownerField`: the end-user owner-scope field (null/absent = none);
|
|
61
|
+
* `orgField`: the workspace-scope field (null/absent = none). */
|
|
42
62
|
export interface CdcCollectionGates {
|
|
43
63
|
readRoleFields?: readonly string[];
|
|
44
64
|
endUserAccess?: 'readwrite' | 'read' | 'none';
|
|
45
65
|
ownerField?: string | null;
|
|
66
|
+
orgField?: string | null;
|
|
46
67
|
}
|
|
47
68
|
/**
|
|
48
69
|
* A `payload: 'full'` change-data rule inlines the stored row into a realtime
|
|
@@ -52,9 +73,21 @@ export interface CdcCollectionGates {
|
|
|
52
73
|
* an `endUserAccess` of 'read' / 'none' (the frame would bypass the
|
|
53
74
|
* collection's end-user gate), or an `ownerField` (end users read only their
|
|
54
75
|
* own rows, but the frame would carry every user's row to every subscriber —
|
|
55
|
-
* CDC-FULL-OWNER-SCOPED)
|
|
76
|
+
* CDC-FULL-OWNER-SCOPED), or an `orgField` (a workspace's rows to every
|
|
77
|
+
* subscriber — `{org}` frames stay ids-only). Pure; one problem line per offending ENABLED
|
|
56
78
|
* rule (an unknown collection is skipped). The safe shape is `payload: 'ids'`
|
|
57
79
|
* and a re-read through the cms, where every gate applies.
|
|
58
80
|
*/
|
|
59
81
|
export declare function cdcFullFramesOverGatedCollections(cdc: unknown, collections: Record<string, CdcCollectionGates>): string[];
|
|
82
|
+
/**
|
|
83
|
+
* WORKSPACE-ROWS Phase 2: a FIXED-channel change-data rule on a workspace
|
|
84
|
+
* collection (live `orgField`) sends every workspace's change ids, status and
|
|
85
|
+
* timing to ONE channel any end user may subscribe to — only the `{org}`
|
|
86
|
+
* template (`<collection>:{org}` → `org:<org_id>:<collection>`) is valid there.
|
|
87
|
+
* The config PUT / draft-publish twin of the `workspaceManifestErrors` rule
|
|
88
|
+
* (which judges only DECLARED collections); the cms runtime floor
|
|
89
|
+
* (cdc.ts cdcChannelFor) drops such a rule for every door that cannot check.
|
|
90
|
+
* Pure; one problem line per offending ENABLED rule.
|
|
91
|
+
*/
|
|
92
|
+
export declare function cdcFixedChannelsOverOrgCollections(cdc: unknown, collections: Record<string, CdcCollectionGates>): string[];
|
|
60
93
|
export {};
|
|
@@ -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<{
|
|
@@ -332,8 +333,10 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
332
333
|
}>>;
|
|
333
334
|
breachedPasswords: import("@sinclair/typebox").TBoolean;
|
|
334
335
|
captchaSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
336
|
+
captchaOnReset: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
335
337
|
allowedRedirectOrigins: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
336
338
|
inviteOnly: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
339
|
+
verifyLinkEvicts: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
337
340
|
allowedEmailDomains: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
338
341
|
}>>;
|
|
339
342
|
}>;
|
|
@@ -410,6 +413,10 @@ export declare const FilesConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
410
413
|
q: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
|
|
411
414
|
}>>>;
|
|
412
415
|
}>>;
|
|
416
|
+
orgScope: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
417
|
+
readRoles: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
418
|
+
writeRoles: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
419
|
+
}>>;
|
|
413
420
|
}>;
|
|
414
421
|
export type FilesConfig = Static<typeof FilesConfigSchema>;
|
|
415
422
|
export declare const DeclaredWebhookSubscriptionSchema: import("@sinclair/typebox").TObject<{
|
|
@@ -579,6 +586,30 @@ export declare const ActivityFeedConfigSchema: import("@sinclair/typebox").TObje
|
|
|
579
586
|
}>;
|
|
580
587
|
}>;
|
|
581
588
|
export type ActivityFeedConfig = Static<typeof ActivityFeedConfigSchema>;
|
|
589
|
+
/** At most this many declared search collections (the converge's worst-case
|
|
590
|
+
* fan-out: one POST per declared entry per push). */
|
|
591
|
+
export declare const SEARCH_COLLECTION_DECLARED_CAP = 50;
|
|
592
|
+
/** One declared search collection — the POST /v1/search/collections
|
|
593
|
+
* body (vector-search-v1 core.ts CreateCollectionIn), keyed by `collection`.
|
|
594
|
+
* An omitted field is NOT compared against a live collection (it takes the
|
|
595
|
+
* vector-search default on create only); `dimensions` are immutable after
|
|
596
|
+
* create — a declared change is a refused row, never a recreate. */
|
|
597
|
+
/** The widths a search collection accepts — = vector-search-v1 backend.ts
|
|
598
|
+
* ALLOWED_DIMS (one indexed vector column each; a parity test pins them), so
|
|
599
|
+
* an unsupported width fails validation instead of only the push. */
|
|
600
|
+
export declare const SEARCH_COLLECTION_DIMS: readonly [256, 384, 512, 768, 1024, 1536, 3072];
|
|
601
|
+
export declare const DeclaredSearchCollectionSchema: import("@sinclair/typebox").TObject<{
|
|
602
|
+
collection: import("@sinclair/typebox").TString;
|
|
603
|
+
dimensions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<256 | 384 | 512 | 768 | 1024 | 1536 | 3072>[]>>;
|
|
604
|
+
embed: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
605
|
+
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">]>>;
|
|
606
|
+
model: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
607
|
+
apiKeyRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
608
|
+
}>>;
|
|
609
|
+
keywordNormalize: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"arabic">, import("@sinclair/typebox").TLiteral<"none">]>>;
|
|
610
|
+
orgScoped: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
611
|
+
}>;
|
|
612
|
+
export type DeclaredSearchCollection = Static<typeof DeclaredSearchCollectionSchema>;
|
|
582
613
|
export declare const VectorSearchConfigSchema: import("@sinclair/typebox").TObject<{
|
|
583
614
|
enabled: import("@sinclair/typebox").TBoolean;
|
|
584
615
|
backend: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"auto">, import("@sinclair/typebox").TLiteral<"lakebase">, import("@sinclair/typebox").TLiteral<"pgvector">]>;
|
|
@@ -616,6 +647,17 @@ export declare const VectorSearchConfigSchema: import("@sinclair/typebox").TObje
|
|
|
616
647
|
statusFilter: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"published">, import("@sinclair/typebox").TLiteral<"all">]>>;
|
|
617
648
|
cron: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
618
649
|
}>>>;
|
|
650
|
+
collections: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
651
|
+
collection: import("@sinclair/typebox").TString;
|
|
652
|
+
dimensions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<256 | 384 | 512 | 768 | 1024 | 1536 | 3072>[]>>;
|
|
653
|
+
embed: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
654
|
+
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">]>>;
|
|
655
|
+
model: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
656
|
+
apiKeyRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
657
|
+
}>>;
|
|
658
|
+
keywordNormalize: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"arabic">, import("@sinclair/typebox").TLiteral<"none">]>>;
|
|
659
|
+
orgScoped: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
660
|
+
}>>>;
|
|
619
661
|
}>;
|
|
620
662
|
export type VectorSearchConfig = Static<typeof VectorSearchConfigSchema>;
|
|
621
663
|
export declare const AI_TEMPLATE_NAME_PATTERN = "^[a-zA-Z0-9_.\\-]+$";
|
|
@@ -766,6 +808,12 @@ export declare const FunctionsConfigSchema: import("@sinclair/typebox").TObject<
|
|
|
766
808
|
metering: import("@sinclair/typebox").TObject<{
|
|
767
809
|
enabled: import("@sinclair/typebox").TBoolean;
|
|
768
810
|
}>;
|
|
811
|
+
logDrain: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
812
|
+
url: import("@sinclair/typebox").TString;
|
|
813
|
+
secretRef: import("@sinclair/typebox").TString;
|
|
814
|
+
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">]>>;
|
|
815
|
+
dailyLines: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
|
|
816
|
+
}>>;
|
|
769
817
|
functions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TObject<{
|
|
770
818
|
bindings: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
771
819
|
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 +968,7 @@ export declare function fnServerScopesError(raw: unknown, fnScopes?: readonly st
|
|
|
920
968
|
* templateVars parity). */
|
|
921
969
|
export declare function rlTemplateVars(template: string): string[];
|
|
922
970
|
export declare function validateDeclaredRlPolicies(policies: readonly DeclaredRlPolicy[] | undefined): string[];
|
|
971
|
+
export declare function validateDeclaredSearchCollections(collections: readonly DeclaredSearchCollection[] | undefined): string[];
|
|
923
972
|
export declare function validateDeclaredOrgRoles(roles: readonly DeclaredOrgRole[] | undefined): string[];
|
|
924
973
|
export declare function validateDeclaredWebhookSubscriptions(subscriptions: readonly DeclaredWebhookSubscription[] | undefined, maxSubscriptions: number | undefined): string[];
|
|
925
974
|
/** The stored-template `schema` bounds — MIRRORED from @vxil/runtime
|
|
@@ -946,3 +995,15 @@ export interface PlannerHint {
|
|
|
946
995
|
dependsOn: FeatureKey[];
|
|
947
996
|
}
|
|
948
997
|
export declare const FEATURE_HINTS: Record<FeatureKey, PlannerHint>;
|
|
998
|
+
/** = cms-v1 core.ts RETAIN_DAYS_MAX (a parity test pins the two). */
|
|
999
|
+
export declare const CMS_RETAIN_DAYS_MAX = 3650;
|
|
1000
|
+
/** A declared `retainDays`: absent = NOT MANAGED (the live value is left as
|
|
1001
|
+
* is — a collection whose retention was set by API is never silently
|
|
1002
|
+
* cleared by a push that does not mention it); `null` = keep items until
|
|
1003
|
+
* deleted; an integer 1..3650 = days. */
|
|
1004
|
+
export declare function isDeclaredRetainDays(v: unknown): v is number | null | undefined;
|
|
1005
|
+
/** Does moving retention from `remote` to `declared` make the sweep delete
|
|
1006
|
+
* MORE items (turning retention on, or shortening it)? On an EXISTING
|
|
1007
|
+
* collection that change applies only under allow-destructive; lengthening
|
|
1008
|
+
* or clearing keeps more and always applies. */
|
|
1009
|
+
export declare function retainDaysShortens(declared: number | null, remote: number | null): boolean;
|
package/dist/config.d.ts
CHANGED
|
@@ -132,13 +132,90 @@ 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
|
+
* - a `features.cms.cdc` rule on an orgField collection must use the
|
|
197
|
+
* `{org}` channel template (Phase 2), stay ids-only, and not target a
|
|
198
|
+
* collection with `orgReadRoles` (the channel admits every member); a
|
|
199
|
+
* template on a declared collection WITHOUT orgField is refused;
|
|
200
|
+
* - a vector-search `sync[]` source reading an orgField collection must
|
|
201
|
+
* target a collection declared in `vector-search.collections[]` with
|
|
202
|
+
* `orgScoped: true` (Phase 2 — each document takes the row's orgField as
|
|
203
|
+
* its org), and an unscoped source never feeds an orgScoped collection;
|
|
204
|
+
* - an orgScoped search collection needs `auth.orgClaims.activeOrg` too;
|
|
205
|
+
* - a read model materializing an orgField source must target an orgField
|
|
206
|
+
* collection whose orgField has the SAME name, and group (or partition) by
|
|
207
|
+
* the source orgField — so each rollup row lands in its own workspace;
|
|
208
|
+
* - an `orgs.roles` key over 32 characters cannot be named in a gate
|
|
209
|
+
* (orgReadRoles / writeRoles / field readRoles) of an orgField collection. */
|
|
210
|
+
export declare function workspaceManifestErrors(config: WorkspaceManifestInput,
|
|
211
|
+
/** `checkAuth: false` skips the activeOrg rule — `/v1/apply` passes it when
|
|
212
|
+
* the bundle neither declares an orgField collection nor features.auth, so
|
|
213
|
+
* pre-existing live state alone never blocks an unrelated apply. */
|
|
214
|
+
opts?: {
|
|
215
|
+
checkAuth?: boolean;
|
|
216
|
+
}): string[];
|
|
217
|
+
/** Throwing form of `workspaceManifestErrors` — run by `defineConfig`. */
|
|
218
|
+
export declare function assertWorkspaceManifest(config: WorkspaceManifestInput): void;
|
|
142
219
|
/** The auth lifecycle events an `authHook` binding may subscribe to
|
|
143
220
|
* (2026-09-10) — a CLOSED union; ONE event per binding, one audit
|
|
144
221
|
* event → one function invocation, no branching inside vxil:
|
package/dist/config.js
CHANGED
|
@@ -16,6 +16,166 @@ 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 isOrgTemplateChannel = (ch) => typeof ch === "string" && ch.split(":").includes("{org}");
|
|
84
|
+
var orgReadRolesOfDef = (def) => isObj(def) ? def.orgReadRoles ?? def.org_read_roles : void 0;
|
|
85
|
+
var ownerFieldOfDef = (def) => {
|
|
86
|
+
const v = isObj(def) ? def.ownerField ?? def.owner_field : void 0;
|
|
87
|
+
return typeof v === "string" && v.length > 0;
|
|
88
|
+
};
|
|
89
|
+
var asList = (v) => Array.isArray(v) ? v : v === void 0 || v === null ? [] : [v];
|
|
90
|
+
var stringsOf = (v) => asList(v).filter((s) => typeof s === "string");
|
|
91
|
+
function workspaceManifestErrors(config, opts = {}) {
|
|
92
|
+
const errs = [];
|
|
93
|
+
const rawCollections = config.cms?.collections;
|
|
94
|
+
const collections = isObj(rawCollections) ? rawCollections : {};
|
|
95
|
+
const orgCols = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const [name, def] of Object.entries(collections)) {
|
|
97
|
+
const of = declaredOrgField(def);
|
|
98
|
+
if (of) orgCols.set(name, of);
|
|
99
|
+
}
|
|
100
|
+
const features = isObj(config.features) ? config.features : {};
|
|
101
|
+
const cms = isObj(features.cms) ? features.cms : {};
|
|
102
|
+
const cdcRules = Object.entries(isObj(cms.cdc) ? cms.cdc : {});
|
|
103
|
+
for (const [rule, r] of cdcRules) {
|
|
104
|
+
if (isObj(r) && typeof r.collection === "string" && isOrgTemplateChannel(r.channel) && Object.prototype.hasOwnProperty.call(collections, r.collection) && !orgCols.has(r.collection)) {
|
|
105
|
+
errs.push(`features.cms.cdc.${rule}: a '{org}' channel needs an orgField collection \u2014 '${r.collection}' declares no orgField, so no row has a workspace to publish to`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const vs = isObj(features["vector-search"]) ? features["vector-search"] : {};
|
|
109
|
+
const orgScopedTargets = new Set(asList(vs.collections).filter((c) => isObj(c) && (c.orgScoped === true || c.org_scoped === true) && typeof c.collection === "string").map((c) => c.collection));
|
|
110
|
+
if (orgCols.size === 0 && orgScopedTargets.size === 0) return errs;
|
|
111
|
+
const names = [...orgCols.keys()].join(", ");
|
|
112
|
+
const auth = isObj(features.auth) ? features.auth : {};
|
|
113
|
+
const oc = isObj(auth.orgClaims) ? auth.orgClaims : {};
|
|
114
|
+
if (opts.checkAuth !== false && (oc.enabled !== true || oc.activeOrg !== true)) {
|
|
115
|
+
if (orgCols.size) {
|
|
116
|
+
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`);
|
|
117
|
+
}
|
|
118
|
+
if (orgScopedTargets.size) {
|
|
119
|
+
errs.push(`vector-search.collections ${[...orgScopedTargets].join(", ")} set orgScoped, which needs features.auth.orgClaims { enabled: true, activeOrg: true } \u2014 without an active org no end user can read them`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const [rule, r] of cdcRules) {
|
|
123
|
+
if (!isObj(r) || typeof r.collection !== "string" || !orgCols.has(r.collection)) continue;
|
|
124
|
+
if (!isOrgTemplateChannel(r.channel)) {
|
|
125
|
+
errs.push(`features.cms.cdc.${rule}: collection '${r.collection}' is workspace-scoped (orgField) \u2014 a fixed cdc channel is not access control; use a workspace channel template, e.g. channel: '${r.collection}:{org}' (each row publishes to org:<its org id>:${r.collection}, which only members of that workspace can subscribe to)`);
|
|
126
|
+
} else if (r.payload === "full") {
|
|
127
|
+
errs.push(`features.cms.cdc.${rule}: collection '${r.collection}' is workspace-scoped (orgField) \u2014 frames stay ids-only (payload: 'ids'); subscribers re-read through cms, where the workspace predicate applies`);
|
|
128
|
+
} else if (stringsOf(orgReadRolesOfDef(collections[r.collection])).length > 0) {
|
|
129
|
+
errs.push(`features.cms.cdc.${rule}: collection '${r.collection}' sets orgReadRoles \u2014 an org:<id> channel admits every member of the workspace, so its frames (item ids, status, timing) would reach roles that cannot read the collection; drop the rule or the orgReadRoles`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
asList(vs.sync).forEach((s, i) => {
|
|
133
|
+
if (!isObj(s) || typeof s.cmsCollection !== "string") return;
|
|
134
|
+
const target = typeof s.collection === "string" ? s.collection : "";
|
|
135
|
+
if (orgCols.has(s.cmsCollection) && !orgScopedTargets.has(target)) {
|
|
136
|
+
errs.push(`features.vector-search.sync[${i}]: cmsCollection '${s.cmsCollection}' is workspace-scoped (orgField) \u2014 the target '${target}' must be declared in vector-search.collections[] with orgScoped: true, or every workspace's rows become searchable by any end user`);
|
|
137
|
+
} else if (orgCols.has(s.cmsCollection) && (stringsOf(orgReadRolesOfDef(collections[s.cmsCollection])).length > 0 || ownerFieldOfDef(collections[s.cmsCollection]))) {
|
|
138
|
+
const which = stringsOf(orgReadRolesOfDef(collections[s.cmsCollection])).length > 0 ? "orgReadRoles" : "ownerField";
|
|
139
|
+
errs.push(`features.vector-search.sync[${i}]: cmsCollection '${s.cmsCollection}' sets ${which} \u2014 synced documents are shared by every member of the workspace, so a member could search rows cms refuses them; drop the sync or the ${which}`);
|
|
140
|
+
} else if (!orgCols.has(s.cmsCollection) && orgScopedTargets.has(target)) {
|
|
141
|
+
errs.push(`features.vector-search.sync[${i}]: target '${target}' is orgScoped but cmsCollection '${s.cmsCollection}' is not workspace-scoped (orgField) \u2014 its documents would carry no workspace`);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
for (const [rmName, rm] of Object.entries(isObj(cms.readModels) ? cms.readModels : {})) {
|
|
145
|
+
if (!isObj(rm) || typeof rm.collection !== "string") continue;
|
|
146
|
+
const srcOrg = orgCols.get(rm.collection);
|
|
147
|
+
const to = isObj(rm.materialize) ? rm.materialize.to : void 0;
|
|
148
|
+
if (!srcOrg || typeof to !== "string") continue;
|
|
149
|
+
const tOrg = orgCols.get(to);
|
|
150
|
+
const spec = isObj(rm.spec) ? rm.spec : {};
|
|
151
|
+
const groups = [...stringsOf(spec.groupBy), ...stringsOf(spec.partitionBy)];
|
|
152
|
+
if (tOrg !== srcOrg || !groups.includes(srcOrg)) {
|
|
153
|
+
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`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const orgs = isObj(features.orgs) ? features.orgs : {};
|
|
157
|
+
const longRoles = new Set(asList(orgs.roles).map((r) => isObj(r) ? r.role_key : void 0).filter((k) => typeof k === "string" && k.length > 32));
|
|
158
|
+
for (const [name] of orgCols) {
|
|
159
|
+
const def = collections[name];
|
|
160
|
+
const gate = /* @__PURE__ */ new Set([
|
|
161
|
+
...stringsOf(def.orgReadRoles ?? def.org_read_roles),
|
|
162
|
+
...stringsOf(def.writeRoles ?? def.write_roles)
|
|
163
|
+
]);
|
|
164
|
+
for (const f of Object.values(isObj(def.fields) ? def.fields : {})) {
|
|
165
|
+
if (isObj(f)) for (const r of stringsOf(f.readRoles ?? f.read_roles)) gate.add(r);
|
|
166
|
+
}
|
|
167
|
+
for (const r of gate) {
|
|
168
|
+
if (r.length > 32 || longRoles.has(r)) {
|
|
169
|
+
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)`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return errs;
|
|
174
|
+
}
|
|
175
|
+
function assertWorkspaceManifest(config) {
|
|
176
|
+
const errs = workspaceManifestErrors(config);
|
|
177
|
+
if (errs.length) throw new Error(`vxil.config: ${errs.join("; ")}`);
|
|
178
|
+
}
|
|
19
179
|
var RELEASED_API_VERSIONS = ["v1"];
|
|
20
180
|
function defineConfig(c) {
|
|
21
181
|
if (c.apiVersion !== void 0 && !RELEASED_API_VERSIONS.includes(c.apiVersion)) {
|
|
@@ -23,7 +183,11 @@ function defineConfig(c) {
|
|
|
23
183
|
`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
184
|
);
|
|
25
185
|
}
|
|
26
|
-
for (const [name, def] of Object.entries(c.cms?.collections ?? {}))
|
|
186
|
+
for (const [name, def] of Object.entries(c.cms?.collections ?? {})) {
|
|
187
|
+
assertIndexedFields(name, def);
|
|
188
|
+
assertWorkspaceCollection(name, def);
|
|
189
|
+
}
|
|
190
|
+
assertWorkspaceManifest(c);
|
|
27
191
|
return c;
|
|
28
192
|
}
|
|
29
193
|
function field(type, opts = {}) {
|
|
@@ -31,13 +195,21 @@ function field(type, opts = {}) {
|
|
|
31
195
|
}
|
|
32
196
|
function collection(def) {
|
|
33
197
|
assertIndexedFields(def.singular ?? "collection", def);
|
|
198
|
+
assertWorkspaceCollection(def.singular ?? "collection", def);
|
|
34
199
|
return def;
|
|
35
200
|
}
|
|
36
201
|
export {
|
|
37
202
|
CMS_INDEXED_FIELD_BUDGET,
|
|
203
|
+
CMS_ORG_READ_ROLES_MAX,
|
|
204
|
+
CMS_ROLE_SLUG_RE,
|
|
38
205
|
RELEASED_API_VERSIONS,
|
|
39
206
|
assertIndexedFields,
|
|
207
|
+
assertWorkspaceCollection,
|
|
208
|
+
assertWorkspaceManifest,
|
|
40
209
|
collection,
|
|
210
|
+
declaredOrgField,
|
|
41
211
|
defineConfig,
|
|
42
|
-
field
|
|
212
|
+
field,
|
|
213
|
+
workspaceCollectionErrors,
|
|
214
|
+
workspaceManifestErrors
|
|
43
215
|
};
|