@vxil/feature-configs 0.1.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/LICENSE +21 -0
- package/dist/hooks.d.ts +167 -0
- package/dist/hooks.js +914 -0
- package/dist/index.d.ts +587 -0
- package/dist/index.js +1191 -0
- package/dist/readmodels.d.ts +38 -0
- package/dist/readmodels.js +229 -0
- package/package.json +30 -0
- package/src/hooks.test.ts +451 -0
- package/src/hooks.ts +830 -0
- package/src/index.test.ts +739 -0
- package/src/index.ts +1572 -0
- package/src/readmodels.ts +248 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 techmaker.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
export declare const HOOK_LIMITS: {
|
|
2
|
+
readonly maxSourceLen: 2000;
|
|
3
|
+
readonly maxTokens: 600;
|
|
4
|
+
readonly maxNodes: 250;
|
|
5
|
+
readonly maxDepth: 40;
|
|
6
|
+
readonly maxEvalSteps: 5000;
|
|
7
|
+
readonly maxStringLen: 4096;
|
|
8
|
+
readonly maxNumberMagnitude: 1000000000000000;
|
|
9
|
+
readonly maxReadEvalStepsPerRequest: 250000;
|
|
10
|
+
readonly maxReadHooksPerCollection: 10;
|
|
11
|
+
};
|
|
12
|
+
/** The ONLY root variables an expression may reference. `caller` is the VERIFIED
|
|
13
|
+
* end-user principal (docs/end-user-principals-design.md §5.6) — read-only and
|
|
14
|
+
* populated ONLY on the READ path (runReadHooks); on the write path and in
|
|
15
|
+
* server-caller mode it is null, exactly like `before` on a create. It carries
|
|
16
|
+
* `caller.endUserId` (the verified session sub, or null) and `caller.principal`
|
|
17
|
+
* ('end_user' | 'tenant'). It is the missing "caller/session context" a read
|
|
18
|
+
* hook keys on to redact/derive per-viewer. */
|
|
19
|
+
export declare const HOOK_ROOT_VARS: readonly ["item", "before", "now", "caller"];
|
|
20
|
+
/** The ONLY callable functions. Each is pure + deterministic + bounded. */
|
|
21
|
+
export declare const HOOK_FUNCTIONS: readonly ["min", "max", "abs", "round", "floor", "ceil", "sqrt", "pow", "sign", "len", "lower", "upper", "trim", "substr", "contains", "startsWith", "endsWith", "concat", "coalesce", "ifNull", "not", "isNull", "number", "string", "bool", "daysBetween", "yearsBetween"];
|
|
22
|
+
export type Node = {
|
|
23
|
+
t: 'num';
|
|
24
|
+
v: number;
|
|
25
|
+
} | {
|
|
26
|
+
t: 'str';
|
|
27
|
+
v: string;
|
|
28
|
+
} | {
|
|
29
|
+
t: 'bool';
|
|
30
|
+
v: boolean;
|
|
31
|
+
} | {
|
|
32
|
+
t: 'null';
|
|
33
|
+
} | {
|
|
34
|
+
t: 'var';
|
|
35
|
+
name: string;
|
|
36
|
+
} | {
|
|
37
|
+
t: 'member';
|
|
38
|
+
obj: Node;
|
|
39
|
+
prop: string;
|
|
40
|
+
} | {
|
|
41
|
+
t: 'unary';
|
|
42
|
+
op: '!' | '-';
|
|
43
|
+
arg: Node;
|
|
44
|
+
} | {
|
|
45
|
+
t: 'bin';
|
|
46
|
+
op: BinOp;
|
|
47
|
+
l: Node;
|
|
48
|
+
r: Node;
|
|
49
|
+
} | {
|
|
50
|
+
t: 'tern';
|
|
51
|
+
c: Node;
|
|
52
|
+
a: Node;
|
|
53
|
+
b: Node;
|
|
54
|
+
} | {
|
|
55
|
+
t: 'call';
|
|
56
|
+
fn: string;
|
|
57
|
+
args: Node[];
|
|
58
|
+
};
|
|
59
|
+
type BinOp = '||' | '&&' | '==' | '!=' | '<' | '<=' | '>' | '>=' | '+' | '-' | '*' | '/' | '%';
|
|
60
|
+
export declare class HookParseError extends Error {
|
|
61
|
+
}
|
|
62
|
+
export declare class HookEvalError extends Error {
|
|
63
|
+
}
|
|
64
|
+
/** Thrown by a `validate` hook whose expression is falsy — maps to a clean 422. */
|
|
65
|
+
export declare class HookRejection extends Error {
|
|
66
|
+
}
|
|
67
|
+
/** Parse a hook expression into an AST. Throws HookParseError on any malformed input. */
|
|
68
|
+
export declare function parseExpr(src: string): Node;
|
|
69
|
+
/** Walk the AST and reject anything outside the allow-list, plus depth bounds.
|
|
70
|
+
* Returns an array of human-readable errors (empty = safe). Pure, no eval. */
|
|
71
|
+
export declare function validateAst(root: Node): string[];
|
|
72
|
+
/** The VERIFIED caller/session context for read hooks (design §5.6). Read-only;
|
|
73
|
+
* present only on the read path in end-user mode. In server mode / write path
|
|
74
|
+
* the whole object is null. */
|
|
75
|
+
export interface HookCaller {
|
|
76
|
+
/** the verified end-user session sub, or null in server-caller mode. */
|
|
77
|
+
endUserId: string | null;
|
|
78
|
+
/** 'end_user' when a session was verified at the edge, else 'tenant'. */
|
|
79
|
+
principal: 'end_user' | 'tenant';
|
|
80
|
+
}
|
|
81
|
+
export interface HookContext {
|
|
82
|
+
item: Record<string, unknown>;
|
|
83
|
+
before: Record<string, unknown> | null;
|
|
84
|
+
now: string;
|
|
85
|
+
/** verified caller/session context (read path only); null otherwise. */
|
|
86
|
+
caller?: HookCaller | null;
|
|
87
|
+
}
|
|
88
|
+
/** A mutable step counter SHARED across many evalExpr calls (the read path's
|
|
89
|
+
* whole-page budget). Checked against maxReadEvalStepsPerRequest. */
|
|
90
|
+
export interface EvalBudget {
|
|
91
|
+
steps: number;
|
|
92
|
+
}
|
|
93
|
+
/** Evaluate a validated AST against the context. Bounded by a per-expression
|
|
94
|
+
* eval-step budget, plus an optional SHARED budget spanning many evaluations
|
|
95
|
+
* (runReadHooks passes one per request so a page of rows shares one bound). */
|
|
96
|
+
export declare function evalExpr(root: Node, ctx: HookContext, shared?: EvalBudget): unknown;
|
|
97
|
+
export interface HookDef {
|
|
98
|
+
collection: string;
|
|
99
|
+
event: 'beforeCreate' | 'beforeUpdate' | 'beforeWrite' | 'beforeRead' | 'afterRead';
|
|
100
|
+
kind: 'validate' | 'derive' | 'redact';
|
|
101
|
+
expr: string;
|
|
102
|
+
field?: string;
|
|
103
|
+
message?: string;
|
|
104
|
+
enabled?: boolean;
|
|
105
|
+
}
|
|
106
|
+
/** Config-WRITE-time validation of one hook (the anti-malice gate). Returns
|
|
107
|
+
* errors (empty = safe to publish). Does NOT need the collection's field list. */
|
|
108
|
+
export declare function validateHookDef(id: string, def: HookDef): string[];
|
|
109
|
+
/** Validate a whole hooks bag (CmsConfig.hooks) at config time. */
|
|
110
|
+
export declare function validateHooksConfig(hooks: Record<string, HookDef> | undefined): string[];
|
|
111
|
+
export interface HookRunResult {
|
|
112
|
+
data: Record<string, unknown>;
|
|
113
|
+
derived: string[];
|
|
114
|
+
}
|
|
115
|
+
/** Run all matching write hooks for (collection, phase) over the candidate data.
|
|
116
|
+
* Mutates a CLONE: derive hooks set fields; a validate hook that returns falsy
|
|
117
|
+
* throws HookRejection (→ the caller maps to a 422 and the tx rolls back).
|
|
118
|
+
* Throws HookEvalError on a runtime fault (also a clean 422, never a 500). */
|
|
119
|
+
export declare function runWriteHooks(hooks: Record<string, HookDef> | undefined, collection: string, phase: 'create' | 'update', data: Record<string, unknown>, before: Record<string, unknown> | null, now: string): HookRunResult;
|
|
120
|
+
export interface ReadHookResult {
|
|
121
|
+
/** Surviving rows in input order. Each is a SHALLOW COPY with a copied `data`
|
|
122
|
+
* bag — the caller's rows are never mutated and nothing here is persisted. */
|
|
123
|
+
rows: Record<string, unknown>[];
|
|
124
|
+
/** Rows removed by a falsy beforeRead validate (the visibility filter). */
|
|
125
|
+
dropped: number;
|
|
126
|
+
}
|
|
127
|
+
/** Run the tenant's READ hooks for a collection over a page of PROJECTED rows
|
|
128
|
+
* (each row an item envelope whose `data` already carries computed fields and
|
|
129
|
+
* $expand results — that is the cross-field surface an expression reads).
|
|
130
|
+
*
|
|
131
|
+
* Per row, in order:
|
|
132
|
+
* 1. beforeRead `validate` — a falsy expression DROPS the row (visibility
|
|
133
|
+
* filter; a dropped row is indistinguishable from a missing one).
|
|
134
|
+
* 2. afterRead `derive` — sets data[field] on the OUTPUT copy only
|
|
135
|
+
* (scalar-only, same guard as the write path; NEVER persisted).
|
|
136
|
+
* 3. afterRead `redact` — deletes data[field] when the expression is truthy.
|
|
137
|
+
*
|
|
138
|
+
* Context per row: { item: the projected data, before: null, now: ONE ISO
|
|
139
|
+
* string for the whole request } — deterministic across the page. The entire
|
|
140
|
+
* page shares ONE eval-step budget (HOOK_LIMITS.maxReadEvalStepsPerRequest),
|
|
141
|
+
* which is the list path's per-request CPU bound; exhausting it throws
|
|
142
|
+
* HookEvalError (→ a clean 422, never a pinned isolate). Write events never
|
|
143
|
+
* fire here, and read events never fire in runWriteHooks. */
|
|
144
|
+
export declare function runReadHooks(hooks: Record<string, HookDef> | undefined, collection: string, rows: ReadonlyArray<Record<string, unknown>>, now: string,
|
|
145
|
+
/** verified caller/session context (design §5.6) — read-only, exposed to
|
|
146
|
+
* expressions as `caller.endUserId` / `caller.principal`. Omitted ⇒ null
|
|
147
|
+
* (server-caller mode). */
|
|
148
|
+
caller?: HookCaller | null): ReadHookResult;
|
|
149
|
+
export interface ExprRef {
|
|
150
|
+
root: 'item' | 'before' | 'now' | 'caller';
|
|
151
|
+
/** dotted path under the root, e.g. item.profile.age → ['profile','age']. */
|
|
152
|
+
path: string[];
|
|
153
|
+
}
|
|
154
|
+
/** Collect every variable reference an expression reads (its inputs). */
|
|
155
|
+
export declare function extractRefs(root: Node): ExprRef[];
|
|
156
|
+
export type InferredType = 'number' | 'string' | 'boolean' | 'null' | 'unknown';
|
|
157
|
+
/** Best-effort static type of an expression's result (its output). 'unknown' for
|
|
158
|
+
* member access / coalesce / mixed ternaries — never a false-positive mismatch. */
|
|
159
|
+
export declare function inferType(nd: Node): InferredType;
|
|
160
|
+
export type HlKind = 'num' | 'str' | 'fn' | 'var' | 'kw' | 'op' | 'ident' | 'ws' | 'err';
|
|
161
|
+
/** Token-level highlighter over the FULL source (whitespace + bad chars kept),
|
|
162
|
+
* so a dashboard editor can render a colored overlay behind a textarea. */
|
|
163
|
+
export declare function highlightTokens(src: string): Array<{
|
|
164
|
+
value: string;
|
|
165
|
+
kind: HlKind;
|
|
166
|
+
}>;
|
|
167
|
+
export {};
|