@vxil/feature-configs 0.4.1 → 0.5.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/apiState.d.ts +181 -0
- package/dist/apiState.js +219 -0
- package/dist/index.d.ts +87 -5
- package/dist/index.js +293 -10
- package/package.json +1 -1
- package/src/apiState.ts +347 -0
- package/src/index.ts +323 -28
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { DeclaredRlPolicy, DeclaredWebhookSubscription } from './index.js';
|
|
2
|
+
/** The target_url path markers of the platform's function-delivery lanes
|
|
3
|
+
* (control-plane fnCmsHooks.ts FN_CMSHOOK_PATH / FN_AUTHHOOK_PATH /
|
|
4
|
+
* FN_WEBHOOK_TRIGGER_PATH — mirrored here because this package is
|
|
5
|
+
* typebox-only). A subscription on one of them is derived from the functions
|
|
6
|
+
* manifest by the deploy reconciler: never declarable, never "undeclared". */
|
|
7
|
+
export declare const FN_TRIGGER_TARGET_MARKERS: readonly ["/v1/internal/fn/cms-hook/", "/v1/internal/fn/auth-hook/", "/v1/internal/fn/trigger/"];
|
|
8
|
+
export declare function isFnTriggerSubscriptionUrl(targetUrl: string): boolean;
|
|
9
|
+
export type ApiStateDatum = 'policies' | 'subscriptions' | 'templates';
|
|
10
|
+
/** feature → the declared-API-state datum its config block may carry. */
|
|
11
|
+
export declare const API_STATE_DATUMS: ReadonlyArray<{
|
|
12
|
+
feature: string;
|
|
13
|
+
datum: ApiStateDatum;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function apiStateDatumOf(feature: string): ApiStateDatum | null;
|
|
16
|
+
/** The declared list a validated manifest carries for its feature's datum, or
|
|
17
|
+
* null when the feature has no datum / the block is absent. An absent block
|
|
18
|
+
* means "not managed by config" (nothing converges, nothing is reported) —
|
|
19
|
+
* distinct from an EMPTY array, which declares "there should be none". */
|
|
20
|
+
export declare function declaredApiState(feature: string, manifest: Record<string, unknown> | null | undefined): {
|
|
21
|
+
datum: ApiStateDatum;
|
|
22
|
+
declared: unknown[];
|
|
23
|
+
} | null;
|
|
24
|
+
/** ONE row of GET /v1/rate-limits/policies (rate-limits-v1 core.ts Policy). */
|
|
25
|
+
export interface LiveRlPolicy {
|
|
26
|
+
policy_id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
key_template: string;
|
|
29
|
+
limit: number;
|
|
30
|
+
window_seconds: number;
|
|
31
|
+
behavior: string;
|
|
32
|
+
/** absent on a policy stored before the token-bucket pass → 'sliding_window' */
|
|
33
|
+
algorithm?: string;
|
|
34
|
+
}
|
|
35
|
+
/** The PUT /v1/rate-limits/policies/:id body the converge sends (core.ts
|
|
36
|
+
* UpdateBody minus `name`, which is the key). */
|
|
37
|
+
export interface RlPolicyPatch {
|
|
38
|
+
limit?: number;
|
|
39
|
+
window_seconds?: number;
|
|
40
|
+
behavior?: 'block' | 'shape';
|
|
41
|
+
algorithm?: 'sliding_window' | 'token_bucket';
|
|
42
|
+
}
|
|
43
|
+
export declare const RL_DEFAULT_BEHAVIOR: "block";
|
|
44
|
+
export declare const RL_DEFAULT_ALGORITHM: "sliding_window";
|
|
45
|
+
export interface RlPolicyPlan {
|
|
46
|
+
create: DeclaredRlPolicy[];
|
|
47
|
+
update: Array<{
|
|
48
|
+
policy_id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
patch: RlPolicyPatch;
|
|
51
|
+
changed: string[];
|
|
52
|
+
}>;
|
|
53
|
+
/** `key_template` differs — the update route cannot change it, so the only
|
|
54
|
+
* fix is delete+recreate (a NEW policy_id): destructive-shaped. */
|
|
55
|
+
recreate: Array<{
|
|
56
|
+
policy_id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
declared: DeclaredRlPolicy;
|
|
59
|
+
}>;
|
|
60
|
+
unchanged: string[];
|
|
61
|
+
/** live rows the config does not declare (left in place by default) */
|
|
62
|
+
undeclared: Array<{
|
|
63
|
+
policy_id: string;
|
|
64
|
+
name: string;
|
|
65
|
+
}>;
|
|
66
|
+
/** a declared name matched by MORE THAN ONE live row — names are not a
|
|
67
|
+
* unique key on the route, so the converge cannot pick; nothing is touched */
|
|
68
|
+
ambiguous: Array<{
|
|
69
|
+
name: string;
|
|
70
|
+
count: number;
|
|
71
|
+
}>;
|
|
72
|
+
}
|
|
73
|
+
export declare function planRlPolicies(declared: readonly DeclaredRlPolicy[], live: readonly LiveRlPolicy[]): RlPolicyPlan;
|
|
74
|
+
/** ONE row of GET /v1/webhooks/subscriptions (state 'deleted' never listed). */
|
|
75
|
+
export interface LiveWebhookSubscription {
|
|
76
|
+
sub_id: string;
|
|
77
|
+
target_url: string;
|
|
78
|
+
event_prefixes: string[];
|
|
79
|
+
state?: string;
|
|
80
|
+
}
|
|
81
|
+
export interface WebhookSubscriptionPlan {
|
|
82
|
+
create: DeclaredWebhookSubscription[];
|
|
83
|
+
/** the prefix SET differs — there is no update route, so delete+recreate (a
|
|
84
|
+
* NEW sub_id, and the watermark restarts at "now"): destructive-shaped. */
|
|
85
|
+
recreate: Array<{
|
|
86
|
+
sub_id: string;
|
|
87
|
+
target_url: string;
|
|
88
|
+
declared: DeclaredWebhookSubscription;
|
|
89
|
+
}>;
|
|
90
|
+
unchanged: string[];
|
|
91
|
+
/** live rows the config does not declare, EXCLUDING the function-delivery
|
|
92
|
+
* lanes (those derive from the functions manifest) — left in place by default */
|
|
93
|
+
undeclared: Array<{
|
|
94
|
+
sub_id: string;
|
|
95
|
+
target_url: string;
|
|
96
|
+
}>;
|
|
97
|
+
}
|
|
98
|
+
export declare function planWebhookSubscriptions(declared: readonly DeclaredWebhookSubscription[], live: readonly LiveWebhookSubscription[]): WebhookSubscriptionPlan;
|
|
99
|
+
/** ONE row of GET /v1/ai/templates. `content_sha256` is the canonical hash of
|
|
100
|
+
* the LATEST version's {system,user,schema} (@vxil/runtime
|
|
101
|
+
* aiTemplateContentSha256), stamped by ai-v1 since 2026-09-23; absent on an
|
|
102
|
+
* older server. */
|
|
103
|
+
export interface LiveAiTemplate {
|
|
104
|
+
name: string;
|
|
105
|
+
latest_version: number;
|
|
106
|
+
content_sha256?: string;
|
|
107
|
+
}
|
|
108
|
+
/** A declared template with its content hash pre-computed by the caller (the
|
|
109
|
+
* hash is async WebCrypto; this planner stays pure/sync). */
|
|
110
|
+
export interface HashedDeclaredAiTemplate {
|
|
111
|
+
template: string;
|
|
112
|
+
content_sha256: string;
|
|
113
|
+
}
|
|
114
|
+
export interface AiTemplatePlan {
|
|
115
|
+
create: string[];
|
|
116
|
+
/** content differs → POST a new version (monotonic per name) */
|
|
117
|
+
update: string[];
|
|
118
|
+
unchanged: string[];
|
|
119
|
+
/** stored names the config does not declare — there is NO delete route for
|
|
120
|
+
* templates, so these are only ever reported, never removed */
|
|
121
|
+
undeclared: string[];
|
|
122
|
+
/** live rows with no `content_sha256` (an older server): the content cannot
|
|
123
|
+
* be compared, so each is re-put as `update` and named here */
|
|
124
|
+
unknownContent: string[];
|
|
125
|
+
}
|
|
126
|
+
export declare function planAiTemplates(declared: readonly HashedDeclaredAiTemplate[], live: readonly LiveAiTemplate[]): AiTemplatePlan;
|
|
127
|
+
export interface ApiStateSummary {
|
|
128
|
+
datum: ApiStateDatum;
|
|
129
|
+
created: string[];
|
|
130
|
+
updated: string[];
|
|
131
|
+
/** destructive-shaped changes APPLIED under allow-destructive (delete+recreate) */
|
|
132
|
+
recreated: string[];
|
|
133
|
+
unchanged: string[];
|
|
134
|
+
/** live rows the config does not declare — left in place (or, under
|
|
135
|
+
* allow-destructive, listed in `deleted` instead) */
|
|
136
|
+
undeclared: string[];
|
|
137
|
+
/** undeclared rows DELETED under allow-destructive */
|
|
138
|
+
deleted: string[];
|
|
139
|
+
/** destructive-shaped changes NOT applied (need allow-destructive) */
|
|
140
|
+
pending_destructive: string[];
|
|
141
|
+
/** declared keys the converge could not resolve (duplicate live rows) */
|
|
142
|
+
ambiguous: string[];
|
|
143
|
+
/** rows a route refused (a 4xx) — the converge continues past them */
|
|
144
|
+
failed: Array<{
|
|
145
|
+
key: string;
|
|
146
|
+
error: string;
|
|
147
|
+
}>;
|
|
148
|
+
}
|
|
149
|
+
export declare function emptyApiStateSummary(datum: ApiStateDatum): ApiStateSummary;
|
|
150
|
+
/** The `api_state` object a config-write response carries (`PUT /v1/config/
|
|
151
|
+
* :feature` and its dashboard twin, dry-run included — control-plane
|
|
152
|
+
* handlers/apiState.ts apiStateResponseField) and the shape `vxil push` READS
|
|
153
|
+
* back instead of converging a second time (review 2026-09-23, finding 1: the
|
|
154
|
+
* server's converge is the one that ran with the key's scopes; a client-side
|
|
155
|
+
* repeat re-listed a KV-backed datum ~100 ms later and could double-create).
|
|
156
|
+
* `ok:false` + `deferred:true` = the feature worker has not seen the config
|
|
157
|
+
* yet (the client may converge after its own wait); `ok:false` otherwise =
|
|
158
|
+
* a DEFINITE, named refusal (nothing to retry); `note` = converged nothing on
|
|
159
|
+
* purpose (a disabled feature). */
|
|
160
|
+
export interface ApiStateResponseField extends ApiStateSummary {
|
|
161
|
+
ok: boolean;
|
|
162
|
+
dry_run?: boolean;
|
|
163
|
+
deferred?: boolean;
|
|
164
|
+
error?: string;
|
|
165
|
+
note?: string;
|
|
166
|
+
}
|
|
167
|
+
/** The note every executor reports for a DISABLED feature whose block still
|
|
168
|
+
* declares rows: the declaration is kept in the manifest and nothing is
|
|
169
|
+
* converged (a parked feature is not a propagation window — review finding 2). */
|
|
170
|
+
export declare const API_STATE_DISABLED_NOTE = "feature disabled \u2014 declaration kept, not converged";
|
|
171
|
+
/** `GET /v1/rate-limits/policies` is a 100-row HARD cap with no cursor. A list
|
|
172
|
+
* AT the cap is not a complete live set, so planning creates against it would
|
|
173
|
+
* re-POST every declared name the cap hid (review finding 5). Both executors
|
|
174
|
+
* refuse with this message instead. */
|
|
175
|
+
export declare function rlPolicyListCapError(liveCount: number): string | null;
|
|
176
|
+
export declare const RL_POLICY_LIST_CAP = 100;
|
|
177
|
+
/** `+2 created · ~1 updated · =3 unchanged · 1 undeclared (left in place)` —
|
|
178
|
+
* the one-line summary `vxil push` prints and the apply journal notes. */
|
|
179
|
+
export declare function formatApiStateSummary(s: ApiStateSummary): string;
|
|
180
|
+
/** The multi-line detail under the summary: one line per named row. */
|
|
181
|
+
export declare function formatApiStateDetail(s: ApiStateSummary): string[];
|
package/dist/apiState.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/** The target_url path markers of the platform's function-delivery lanes
|
|
2
|
+
* (control-plane fnCmsHooks.ts FN_CMSHOOK_PATH / FN_AUTHHOOK_PATH /
|
|
3
|
+
* FN_WEBHOOK_TRIGGER_PATH — mirrored here because this package is
|
|
4
|
+
* typebox-only). A subscription on one of them is derived from the functions
|
|
5
|
+
* manifest by the deploy reconciler: never declarable, never "undeclared". */
|
|
6
|
+
export const FN_TRIGGER_TARGET_MARKERS = [
|
|
7
|
+
'/v1/internal/fn/cms-hook/',
|
|
8
|
+
'/v1/internal/fn/auth-hook/',
|
|
9
|
+
'/v1/internal/fn/trigger/',
|
|
10
|
+
];
|
|
11
|
+
export function isFnTriggerSubscriptionUrl(targetUrl) {
|
|
12
|
+
return FN_TRIGGER_TARGET_MARKERS.some((m) => targetUrl.includes(m));
|
|
13
|
+
}
|
|
14
|
+
/** feature → the declared-API-state datum its config block may carry. */
|
|
15
|
+
export const API_STATE_DATUMS = [
|
|
16
|
+
{ feature: 'rate-limits', datum: 'policies' },
|
|
17
|
+
{ feature: 'webhooks', datum: 'subscriptions' },
|
|
18
|
+
{ feature: 'ai', datum: 'templates' },
|
|
19
|
+
];
|
|
20
|
+
export function apiStateDatumOf(feature) {
|
|
21
|
+
return API_STATE_DATUMS.find((d) => d.feature === feature)?.datum ?? null;
|
|
22
|
+
}
|
|
23
|
+
/** The declared list a validated manifest carries for its feature's datum, or
|
|
24
|
+
* null when the feature has no datum / the block is absent. An absent block
|
|
25
|
+
* means "not managed by config" (nothing converges, nothing is reported) —
|
|
26
|
+
* distinct from an EMPTY array, which declares "there should be none". */
|
|
27
|
+
export function declaredApiState(feature, manifest) {
|
|
28
|
+
const datum = apiStateDatumOf(feature);
|
|
29
|
+
if (!datum || !manifest)
|
|
30
|
+
return null;
|
|
31
|
+
const list = manifest[datum];
|
|
32
|
+
if (!Array.isArray(list))
|
|
33
|
+
return null;
|
|
34
|
+
return { datum, declared: list };
|
|
35
|
+
}
|
|
36
|
+
export const RL_DEFAULT_BEHAVIOR = 'block';
|
|
37
|
+
export const RL_DEFAULT_ALGORITHM = 'sliding_window';
|
|
38
|
+
export function planRlPolicies(declared, live) {
|
|
39
|
+
const plan = { create: [], update: [], recreate: [], unchanged: [], undeclared: [], ambiguous: [] };
|
|
40
|
+
const byName = new Map();
|
|
41
|
+
for (const row of live)
|
|
42
|
+
byName.set(row.name, [...(byName.get(row.name) ?? []), row]);
|
|
43
|
+
const declaredNames = new Set(declared.map((d) => d.name));
|
|
44
|
+
for (const d of declared) {
|
|
45
|
+
const rows = byName.get(d.name) ?? [];
|
|
46
|
+
if (rows.length === 0) {
|
|
47
|
+
plan.create.push(d);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (rows.length > 1) {
|
|
51
|
+
plan.ambiguous.push({ name: d.name, count: rows.length });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const l = rows[0];
|
|
55
|
+
if (l.key_template !== d.key_template) {
|
|
56
|
+
plan.recreate.push({ policy_id: l.policy_id, name: d.name, declared: d });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const patch = {};
|
|
60
|
+
const changed = [];
|
|
61
|
+
if (d.limit !== undefined && d.limit !== l.limit) {
|
|
62
|
+
patch.limit = d.limit;
|
|
63
|
+
changed.push('limit');
|
|
64
|
+
}
|
|
65
|
+
if (d.window_seconds !== undefined && d.window_seconds !== l.window_seconds) {
|
|
66
|
+
patch.window_seconds = d.window_seconds;
|
|
67
|
+
changed.push('window_seconds');
|
|
68
|
+
}
|
|
69
|
+
const wantBehavior = d.behavior ?? RL_DEFAULT_BEHAVIOR;
|
|
70
|
+
if (wantBehavior !== l.behavior) {
|
|
71
|
+
patch.behavior = wantBehavior;
|
|
72
|
+
changed.push('behavior');
|
|
73
|
+
}
|
|
74
|
+
const wantAlgorithm = d.algorithm ?? RL_DEFAULT_ALGORITHM;
|
|
75
|
+
if (wantAlgorithm !== (l.algorithm ?? RL_DEFAULT_ALGORITHM)) {
|
|
76
|
+
patch.algorithm = wantAlgorithm;
|
|
77
|
+
changed.push('algorithm');
|
|
78
|
+
}
|
|
79
|
+
if (changed.length)
|
|
80
|
+
plan.update.push({ policy_id: l.policy_id, name: d.name, patch, changed });
|
|
81
|
+
else
|
|
82
|
+
plan.unchanged.push(d.name);
|
|
83
|
+
}
|
|
84
|
+
for (const row of live) {
|
|
85
|
+
if (!declaredNames.has(row.name))
|
|
86
|
+
plan.undeclared.push({ policy_id: row.policy_id, name: row.name });
|
|
87
|
+
}
|
|
88
|
+
return plan;
|
|
89
|
+
}
|
|
90
|
+
function sameStringSet(a, b) {
|
|
91
|
+
const A = new Set(a);
|
|
92
|
+
const B = new Set(b);
|
|
93
|
+
return A.size === B.size && [...A].every((x) => B.has(x));
|
|
94
|
+
}
|
|
95
|
+
export function planWebhookSubscriptions(declared, live) {
|
|
96
|
+
const plan = { create: [], recreate: [], unchanged: [], undeclared: [] };
|
|
97
|
+
const declaredUrls = new Set(declared.map((d) => d.target_url));
|
|
98
|
+
const matched = new Set();
|
|
99
|
+
for (const d of declared) {
|
|
100
|
+
const want = d.event_prefixes ?? [];
|
|
101
|
+
const rows = live.filter((l) => l.target_url === d.target_url);
|
|
102
|
+
if (rows.length === 0) {
|
|
103
|
+
plan.create.push(d);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// Prefer an EXACT prefix-set match among duplicates (a hand-made row equal
|
|
107
|
+
// to the declaration is adopted as-is — no churn); else the first row is
|
|
108
|
+
// the one recreated and the rest fall through as undeclared duplicates.
|
|
109
|
+
const exact = rows.find((l) => sameStringSet(l.event_prefixes ?? [], want));
|
|
110
|
+
const chosen = exact ?? rows[0];
|
|
111
|
+
matched.add(chosen.sub_id);
|
|
112
|
+
if (exact)
|
|
113
|
+
plan.unchanged.push(d.target_url);
|
|
114
|
+
else
|
|
115
|
+
plan.recreate.push({ sub_id: chosen.sub_id, target_url: d.target_url, declared: d });
|
|
116
|
+
}
|
|
117
|
+
for (const l of live) {
|
|
118
|
+
if (matched.has(l.sub_id))
|
|
119
|
+
continue;
|
|
120
|
+
if (isFnTriggerSubscriptionUrl(l.target_url))
|
|
121
|
+
continue; // manifest-derived, never ours
|
|
122
|
+
if (declaredUrls.has(l.target_url)) {
|
|
123
|
+
// a duplicate live row for a declared url — surplus, not the adopted one
|
|
124
|
+
plan.undeclared.push({ sub_id: l.sub_id, target_url: l.target_url });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
plan.undeclared.push({ sub_id: l.sub_id, target_url: l.target_url });
|
|
128
|
+
}
|
|
129
|
+
return plan;
|
|
130
|
+
}
|
|
131
|
+
export function planAiTemplates(declared, live) {
|
|
132
|
+
const plan = { create: [], update: [], unchanged: [], undeclared: [], unknownContent: [] };
|
|
133
|
+
const byName = new Map(live.map((l) => [l.name, l]));
|
|
134
|
+
const declaredNames = new Set(declared.map((d) => d.template));
|
|
135
|
+
for (const d of declared) {
|
|
136
|
+
const l = byName.get(d.template);
|
|
137
|
+
if (!l) {
|
|
138
|
+
plan.create.push(d.template);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (l.content_sha256 === undefined) {
|
|
142
|
+
plan.unknownContent.push(d.template);
|
|
143
|
+
plan.update.push(d.template);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (l.content_sha256 === d.content_sha256)
|
|
147
|
+
plan.unchanged.push(d.template);
|
|
148
|
+
else
|
|
149
|
+
plan.update.push(d.template);
|
|
150
|
+
}
|
|
151
|
+
for (const l of live)
|
|
152
|
+
if (!declaredNames.has(l.name))
|
|
153
|
+
plan.undeclared.push(l.name);
|
|
154
|
+
return plan;
|
|
155
|
+
}
|
|
156
|
+
export function emptyApiStateSummary(datum) {
|
|
157
|
+
return {
|
|
158
|
+
datum, created: [], updated: [], recreated: [], unchanged: [], undeclared: [],
|
|
159
|
+
deleted: [], pending_destructive: [], ambiguous: [], failed: [],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** The note every executor reports for a DISABLED feature whose block still
|
|
163
|
+
* declares rows: the declaration is kept in the manifest and nothing is
|
|
164
|
+
* converged (a parked feature is not a propagation window — review finding 2). */
|
|
165
|
+
export const API_STATE_DISABLED_NOTE = 'feature disabled — declaration kept, not converged';
|
|
166
|
+
/** `GET /v1/rate-limits/policies` is a 100-row HARD cap with no cursor. A list
|
|
167
|
+
* AT the cap is not a complete live set, so planning creates against it would
|
|
168
|
+
* re-POST every declared name the cap hid (review finding 5). Both executors
|
|
169
|
+
* refuse with this message instead. */
|
|
170
|
+
export function rlPolicyListCapError(liveCount) {
|
|
171
|
+
if (liveCount < RL_POLICY_LIST_CAP)
|
|
172
|
+
return null;
|
|
173
|
+
return `the policy list is at its ${RL_POLICY_LIST_CAP}-row hard cap (no cursor) — the live set may be incomplete; delete undeclared policies before converging`;
|
|
174
|
+
}
|
|
175
|
+
export const RL_POLICY_LIST_CAP = 100;
|
|
176
|
+
/** `+2 created · ~1 updated · =3 unchanged · 1 undeclared (left in place)` —
|
|
177
|
+
* the one-line summary `vxil push` prints and the apply journal notes. */
|
|
178
|
+
export function formatApiStateSummary(s) {
|
|
179
|
+
const parts = [];
|
|
180
|
+
if (s.created.length)
|
|
181
|
+
parts.push(`+${s.created.length} created`);
|
|
182
|
+
if (s.updated.length)
|
|
183
|
+
parts.push(`~${s.updated.length} updated`);
|
|
184
|
+
if (s.recreated.length)
|
|
185
|
+
parts.push(`↻${s.recreated.length} recreated`);
|
|
186
|
+
parts.push(`=${s.unchanged.length} unchanged`);
|
|
187
|
+
if (s.deleted.length)
|
|
188
|
+
parts.push(`-${s.deleted.length} deleted`);
|
|
189
|
+
if (s.undeclared.length)
|
|
190
|
+
parts.push(`${s.undeclared.length} undeclared (left in place)`);
|
|
191
|
+
if (s.pending_destructive.length)
|
|
192
|
+
parts.push(`${s.pending_destructive.length} destructive-shaped NOT applied (--allow-destructive)`);
|
|
193
|
+
if (s.ambiguous.length)
|
|
194
|
+
parts.push(`${s.ambiguous.length} ambiguous (duplicate live rows — not touched)`);
|
|
195
|
+
if (s.failed.length)
|
|
196
|
+
parts.push(`${s.failed.length} failed`);
|
|
197
|
+
return parts.join(' · ');
|
|
198
|
+
}
|
|
199
|
+
/** The multi-line detail under the summary: one line per named row. */
|
|
200
|
+
export function formatApiStateDetail(s) {
|
|
201
|
+
const lines = [];
|
|
202
|
+
for (const k of s.created)
|
|
203
|
+
lines.push(` + ${k}`);
|
|
204
|
+
for (const k of s.updated)
|
|
205
|
+
lines.push(` ~ ${k}`);
|
|
206
|
+
for (const k of s.recreated)
|
|
207
|
+
lines.push(` ↻ ${k} (recreated — new id)`);
|
|
208
|
+
for (const k of s.deleted)
|
|
209
|
+
lines.push(` - ${k} (deleted: undeclared, --allow-destructive)`);
|
|
210
|
+
for (const k of s.undeclared)
|
|
211
|
+
lines.push(` · ${k} — live but not declared (left in place; --allow-destructive deletes it)`);
|
|
212
|
+
for (const k of s.pending_destructive)
|
|
213
|
+
lines.push(` ! ${k} — needs delete+recreate (new id); re-run with --allow-destructive`);
|
|
214
|
+
for (const k of s.ambiguous)
|
|
215
|
+
lines.push(` ! ${k} — more than one live row carries this name; delete the extras first`);
|
|
216
|
+
for (const f of s.failed)
|
|
217
|
+
lines.push(` ✗ ${f.key} — ${f.error}`);
|
|
218
|
+
return lines;
|
|
219
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Static, type TSchema } from '@sinclair/typebox';
|
|
2
2
|
export * from './hooks.js';
|
|
3
3
|
export * from './readmodels.js';
|
|
4
|
+
export * from './apiState.js';
|
|
4
5
|
export declare const RESERVED_CREDIT_TYPES: ReadonlySet<string>;
|
|
5
6
|
/** True when a credit_type is a vxil-COGS reserved type (grant/consume/config
|
|
6
7
|
* of which is restricted to internal platform machinery). */
|
|
@@ -37,6 +38,12 @@ export declare function validateNotificationOverrides(templates: {
|
|
|
37
38
|
text?: string;
|
|
38
39
|
}>>;
|
|
39
40
|
} | undefined): string[];
|
|
41
|
+
/** AWS region grammar for `notifications.ses.region` (`us-east-1`,
|
|
42
|
+
* `eu-central-1`, `ap-southeast-2`, `us-gov-west-1`, …): two-letter partition,
|
|
43
|
+
* one or more lowercase words, a single digit. Pinned as a pattern rather than
|
|
44
|
+
* a list so a newly launched SES region needs no vxil release; a typo still
|
|
45
|
+
* fails at `vxil push` (TypeBox), never as a DNS error on the first send. */
|
|
46
|
+
export declare const SES_REGION_PATTERN = "^[a-z]{2}(-[a-z]+)+-\\d$";
|
|
40
47
|
export declare const NotificationsConfigSchema: import("@sinclair/typebox").TObject<{
|
|
41
48
|
enabled: import("@sinclair/typebox").TBoolean;
|
|
42
49
|
fromEmail: import("@sinclair/typebox").TString;
|
|
@@ -44,7 +51,12 @@ export declare const NotificationsConfigSchema: import("@sinclair/typebox").TObj
|
|
|
44
51
|
replyTo: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
45
52
|
resendApiKeyRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
46
53
|
webhookSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
47
|
-
provider: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"resend">, import("@sinclair/typebox").TLiteral<"mock">]>;
|
|
54
|
+
provider: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"resend">, import("@sinclair/typebox").TLiteral<"ses">, import("@sinclair/typebox").TLiteral<"mock">]>;
|
|
55
|
+
ses: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
56
|
+
region: import("@sinclair/typebox").TString;
|
|
57
|
+
accessKeyIdRef: import("@sinclair/typebox").TString;
|
|
58
|
+
secretAccessKeyRef: import("@sinclair/typebox").TString;
|
|
59
|
+
}>>;
|
|
48
60
|
defaultLocale: import("@sinclair/typebox").TString;
|
|
49
61
|
retry: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
50
62
|
maxAttempts: import("@sinclair/typebox").TInteger;
|
|
@@ -53,10 +65,10 @@ export declare const NotificationsConfigSchema: import("@sinclair/typebox").TObj
|
|
|
53
65
|
suppression: import("@sinclair/typebox").TObject<{
|
|
54
66
|
softBounceThreshold: import("@sinclair/typebox").TInteger;
|
|
55
67
|
}>;
|
|
56
|
-
rateLimit: import("@sinclair/typebox").TObject<{
|
|
68
|
+
rateLimit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
57
69
|
perDay: import("@sinclair/typebox").TInteger;
|
|
58
70
|
perTenantSec: import("@sinclair/typebox").TInteger;
|
|
59
|
-
}
|
|
71
|
+
}>>;
|
|
60
72
|
templates: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
61
73
|
allowOverride: import("@sinclair/typebox").TBoolean;
|
|
62
74
|
overrides: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TObject<{
|
|
@@ -150,6 +162,17 @@ declare const OidcProviderSchema: import("@sinclair/typebox").TObject<{
|
|
|
150
162
|
allowedDomains: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
151
163
|
autoLink: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
|
|
152
164
|
}>;
|
|
165
|
+
export declare const REDIRECT_ORIGIN_PATTERN = "^(https://[^/?#\\s]+|http://(localhost|127\\.0\\.0\\.1)(:[0-9]{1,5})?)$";
|
|
166
|
+
/** THE scheme rule for a redirect/return URL: https anywhere, http on loopback
|
|
167
|
+
* only. Takes the parsed URL (or any {protocol, hostname} pair) so the config
|
|
168
|
+
* validator and the worker's runtime guard call the identical predicate. */
|
|
169
|
+
export declare function redirectSchemeAllowed(u: {
|
|
170
|
+
protocol: string;
|
|
171
|
+
hostname: string;
|
|
172
|
+
}): boolean;
|
|
173
|
+
/** An allow-list ENTRY is a bare origin that parses and passes the scheme
|
|
174
|
+
* rule. Used by validateFeatureConfig (auth) on top of the coarse pattern. */
|
|
175
|
+
export declare function isAllowedRedirectOriginEntry(entry: string): boolean;
|
|
153
176
|
export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
|
|
154
177
|
enabled: import("@sinclair/typebox").TBoolean;
|
|
155
178
|
methods: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
@@ -243,12 +266,35 @@ export type AuthSecurityConfig = NonNullable<Static<typeof AuthConfigSchema>['se
|
|
|
243
266
|
/** The RB-2 generic OIDC / SSO issuer block (`providers.oidc`). */
|
|
244
267
|
export type AuthOidcConfig = Static<typeof OidcProviderSchema>;
|
|
245
268
|
export type AuthConfig = Static<typeof AuthConfigSchema>;
|
|
269
|
+
export declare const RL_POLICY_NAME_PATTERN = "^[a-zA-Z0-9_.:-]+$";
|
|
270
|
+
/** GET /v1/rate-limits/policies lists at most 100 with no cursor (core.ts
|
|
271
|
+
* listPolicies) — a declared set past that could never be reconciled. */
|
|
272
|
+
export declare const RL_MAX_DECLARED_POLICIES = 100;
|
|
273
|
+
/** rate-limits-v1 core.ts createPolicy: at most 8 distinct `{var}`s. */
|
|
274
|
+
export declare const RL_KEY_TEMPLATE_MAX_VARS = 8;
|
|
275
|
+
export declare const DeclaredRlPolicySchema: import("@sinclair/typebox").TObject<{
|
|
276
|
+
name: import("@sinclair/typebox").TString;
|
|
277
|
+
key_template: import("@sinclair/typebox").TString;
|
|
278
|
+
limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
|
|
279
|
+
window_seconds: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
|
|
280
|
+
behavior: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"block">, import("@sinclair/typebox").TLiteral<"shape">]>>;
|
|
281
|
+
algorithm: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"sliding_window">, import("@sinclair/typebox").TLiteral<"token_bucket">]>>;
|
|
282
|
+
}>;
|
|
283
|
+
export type DeclaredRlPolicy = Static<typeof DeclaredRlPolicySchema>;
|
|
246
284
|
export declare const RateLimitsConfigSchema: import("@sinclair/typebox").TObject<{
|
|
247
285
|
enabled: import("@sinclair/typebox").TBoolean;
|
|
248
286
|
defaults: import("@sinclair/typebox").TObject<{
|
|
249
287
|
perTenantSec: import("@sinclair/typebox").TInteger;
|
|
250
288
|
burstSize: import("@sinclair/typebox").TInteger;
|
|
251
289
|
}>;
|
|
290
|
+
policies: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
291
|
+
name: import("@sinclair/typebox").TString;
|
|
292
|
+
key_template: import("@sinclair/typebox").TString;
|
|
293
|
+
limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
|
|
294
|
+
window_seconds: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
|
|
295
|
+
behavior: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"block">, import("@sinclair/typebox").TLiteral<"shape">]>>;
|
|
296
|
+
algorithm: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"sliding_window">, import("@sinclair/typebox").TLiteral<"token_bucket">]>>;
|
|
297
|
+
}>>>;
|
|
252
298
|
}>;
|
|
253
299
|
export type RateLimitsConfig = Static<typeof RateLimitsConfigSchema>;
|
|
254
300
|
export declare const FilesConfigSchema: import("@sinclair/typebox").TObject<{
|
|
@@ -279,6 +325,11 @@ export declare const FilesConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
279
325
|
}>>;
|
|
280
326
|
}>;
|
|
281
327
|
export type FilesConfig = Static<typeof FilesConfigSchema>;
|
|
328
|
+
export declare const DeclaredWebhookSubscriptionSchema: import("@sinclair/typebox").TObject<{
|
|
329
|
+
target_url: import("@sinclair/typebox").TString;
|
|
330
|
+
event_prefixes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
331
|
+
}>;
|
|
332
|
+
export type DeclaredWebhookSubscription = Static<typeof DeclaredWebhookSubscriptionSchema>;
|
|
282
333
|
export declare const WebhooksConfigSchema: import("@sinclair/typebox").TObject<{
|
|
283
334
|
enabled: import("@sinclair/typebox").TBoolean;
|
|
284
335
|
maxSubscriptions: import("@sinclair/typebox").TInteger;
|
|
@@ -288,6 +339,10 @@ export declare const WebhooksConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
288
339
|
minLevel: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"warn">, import("@sinclair/typebox").TLiteral<"error">]>;
|
|
289
340
|
digestMinutes: import("@sinclair/typebox").TInteger;
|
|
290
341
|
}>>;
|
|
342
|
+
subscriptions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
343
|
+
target_url: import("@sinclair/typebox").TString;
|
|
344
|
+
event_prefixes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
345
|
+
}>>>;
|
|
291
346
|
}>;
|
|
292
347
|
export type WebhooksConfig = Static<typeof WebhooksConfigSchema>;
|
|
293
348
|
export declare const CommentsConfigSchema: import("@sinclair/typebox").TObject<{
|
|
@@ -454,6 +509,15 @@ export declare const VectorSearchConfigSchema: import("@sinclair/typebox").TObje
|
|
|
454
509
|
}>>>;
|
|
455
510
|
}>;
|
|
456
511
|
export type VectorSearchConfig = Static<typeof VectorSearchConfigSchema>;
|
|
512
|
+
export declare const AI_TEMPLATE_NAME_PATTERN = "^[a-zA-Z0-9_.\\-]+$";
|
|
513
|
+
export declare const AI_MAX_DECLARED_TEMPLATES = 50;
|
|
514
|
+
export declare const DeclaredAiTemplateSchema: import("@sinclair/typebox").TObject<{
|
|
515
|
+
template: import("@sinclair/typebox").TString;
|
|
516
|
+
user: import("@sinclair/typebox").TString;
|
|
517
|
+
system: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
518
|
+
schema: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
|
|
519
|
+
}>;
|
|
520
|
+
export type DeclaredAiTemplate = Static<typeof DeclaredAiTemplateSchema>;
|
|
457
521
|
export declare const AiConfigSchema: import("@sinclair/typebox").TObject<{
|
|
458
522
|
enabled: import("@sinclair/typebox").TBoolean;
|
|
459
523
|
defaultProvider: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"mock">, import("@sinclair/typebox").TLiteral<"openai">, import("@sinclair/typebox").TLiteral<"anthropic">, import("@sinclair/typebox").TLiteral<"gemini">, import("@sinclair/typebox").TLiteral<"azure">, import("@sinclair/typebox").TLiteral<"openrouter">]>;
|
|
@@ -478,11 +542,17 @@ export declare const AiConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
478
542
|
tokensPerUserPerDay: import("@sinclair/typebox").TInteger;
|
|
479
543
|
consumeCredits: import("@sinclair/typebox").TBoolean;
|
|
480
544
|
}>;
|
|
481
|
-
streaming: import("@sinclair/typebox").TObject<{
|
|
545
|
+
streaming: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
482
546
|
enabled: import("@sinclair/typebox").TBoolean;
|
|
483
547
|
replayBufferFrames: import("@sinclair/typebox").TInteger;
|
|
484
548
|
flushMs: import("@sinclair/typebox").TInteger;
|
|
485
|
-
}
|
|
549
|
+
}>>;
|
|
550
|
+
templates: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
551
|
+
template: import("@sinclair/typebox").TString;
|
|
552
|
+
user: import("@sinclair/typebox").TString;
|
|
553
|
+
system: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
554
|
+
schema: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
|
|
555
|
+
}>>>;
|
|
486
556
|
}>;
|
|
487
557
|
export type AiConfig = Static<typeof AiConfigSchema>;
|
|
488
558
|
export declare const RagConfigSchema: import("@sinclair/typebox").TObject<{
|
|
@@ -691,6 +761,18 @@ export declare const MCP_MAX_PROMPTS = 32;
|
|
|
691
761
|
* stays ALLOWED (fn→fn composition). Source of truth for both the config-write
|
|
692
762
|
* validation below and the control-plane deploy clamp. */
|
|
693
763
|
export declare const DENY_FUNCTION_SCOPES: Set<string>;
|
|
764
|
+
/** `{var}` names of a rate-limit key template (rate-limits-v1 core.ts
|
|
765
|
+
* templateVars parity). */
|
|
766
|
+
export declare function rlTemplateVars(template: string): string[];
|
|
767
|
+
export declare function validateDeclaredRlPolicies(policies: readonly DeclaredRlPolicy[] | undefined): string[];
|
|
768
|
+
export declare function validateDeclaredWebhookSubscriptions(subscriptions: readonly DeclaredWebhookSubscription[] | undefined, maxSubscriptions: number | undefined): string[];
|
|
769
|
+
/** The stored-template `schema` bounds — MIRRORED from @vxil/runtime
|
|
770
|
+
* (AI_TEMPLATE_SCHEMA_MAX_BYTES / AI_TEMPLATE_SCHEMA_MAX_DEPTH; this package is
|
|
771
|
+
* typebox-only). ai-v1 putTemplate refuses the same numbers at the route, so a
|
|
772
|
+
* declared entry that passes here is never a 422 at converge. */
|
|
773
|
+
export declare const AI_TEMPLATE_SCHEMA_MAX_BYTES = 32000;
|
|
774
|
+
export declare const AI_TEMPLATE_SCHEMA_MAX_DEPTH = 64;
|
|
775
|
+
export declare function validateDeclaredAiTemplates(templates: readonly DeclaredAiTemplate[] | undefined): string[];
|
|
694
776
|
export declare function validateFeatureConfig(feature: string, raw: unknown): ConfigValidation;
|
|
695
777
|
export declare const FEATURE_KEYS: readonly ["notifications", "jobs", "auth", "rate-limits", "files", "webhooks", "comments", "cms", "mcp", "realtime", "presence", "orgs", "activity-feed", "vector-search", "ai", "rag", "payments", "functions", "copilot"];
|
|
696
778
|
export type FeatureKey = (typeof FEATURE_KEYS)[number];
|