@tillstack/cli 0.2.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/rules.js ADDED
@@ -0,0 +1,175 @@
1
+ // Rules-as-code for TillShield's inline WAF.
2
+ //
3
+ // The dashboard is fine for one-off tweaks, but a security team wants its edge
4
+ // rules in version control: reviewed in a PR, diffed, rolled back with `git
5
+ // revert`, applied from CI. This module turns the server's rule set into a flat,
6
+ // stable document and reconciles a desired document back onto the server.
7
+ //
8
+ // The natural key is the rule NAME (unique per org by convention). Apply matches
9
+ // desired↔existing by name and produces a create / update / delete plan:
10
+ // • in file, not on server → create
11
+ // • in both, content differs → update (PATCH by id)
12
+ // • in both, content identical → unchanged (no request)
13
+ // • on server, not in file → delete — ONLY with --prune
14
+ //
15
+ // `export` then immediate `apply` must be a no-op, so export and the diff share
16
+ // one canonical form (canonicalRule). We are deliberately dependency-free — the
17
+ // document is JSON, which is diffable, PR-reviewable and needs no parser.
18
+ export const RULES_DOC_VERSION = 1;
19
+ // Field order is fixed so a stringified rule is stable regardless of how the
20
+ // server (or a hand-edited file) happened to order keys.
21
+ const MATCH_KEYS = [
22
+ 'ip_cidrs',
23
+ 'paths',
24
+ 'methods',
25
+ 'countries',
26
+ 'user_agent_regex',
27
+ 'threat_intel',
28
+ ];
29
+ function canonicalMatch(m) {
30
+ const out = {};
31
+ if (!m || typeof m !== 'object')
32
+ return out;
33
+ for (const k of MATCH_KEYS) {
34
+ const v = m[k];
35
+ if (v === undefined || v === null)
36
+ continue;
37
+ // Drop empty arrays / empty strings so "unset" and "set to nothing" match.
38
+ if (Array.isArray(v)) {
39
+ if (v.length > 0)
40
+ out[k] = v;
41
+ }
42
+ else if (typeof v === 'string') {
43
+ if (v.length > 0)
44
+ out[k] = v;
45
+ }
46
+ else {
47
+ ;
48
+ out[k] = v;
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+ function canonicalRateLimit(r) {
54
+ if (!r || typeof r !== 'object')
55
+ return null;
56
+ if (typeof r.requests !== 'number' || typeof r.window_seconds !== 'number')
57
+ return null;
58
+ return { requests: r.requests, window_seconds: r.window_seconds, by: r.by ?? 'ip' };
59
+ }
60
+ /** Normalize any rule-shaped object (server row OR file entry) to canonical form. */
61
+ export function canonicalRule(r) {
62
+ return {
63
+ name: String(r.name ?? '').trim(),
64
+ description: r.description ?? null,
65
+ project_id: r.project_id ?? null,
66
+ enabled: r.enabled !== false,
67
+ priority: typeof r.priority === 'number' ? r.priority : 100,
68
+ mode: (r.mode ?? 'block'),
69
+ match: canonicalMatch(r.match),
70
+ rate_limit: canonicalRateLimit(r.rate_limit),
71
+ response_status: typeof r.response_status === 'number' ? r.response_status : 403,
72
+ response_body: r.response_body ?? null,
73
+ escalate_to_incident: r.escalate_to_incident === true,
74
+ };
75
+ }
76
+ /** Deterministic JSON (keys sorted) so two canonical rules compare by value. */
77
+ export function stableStringify(value) {
78
+ return JSON.stringify(sortKeys(value));
79
+ }
80
+ function sortKeys(v) {
81
+ if (Array.isArray(v))
82
+ return v.map(sortKeys);
83
+ if (v && typeof v === 'object') {
84
+ const out = {};
85
+ for (const k of Object.keys(v).sort()) {
86
+ out[k] = sortKeys(v[k]);
87
+ }
88
+ return out;
89
+ }
90
+ return v;
91
+ }
92
+ /** Serialize the server's rules into an authored document. */
93
+ export function toDoc(rules) {
94
+ return {
95
+ version: RULES_DOC_VERSION,
96
+ rules: rules.map(canonicalRule).sort((a, b) => a.priority - b.priority || a.name.localeCompare(b.name)),
97
+ };
98
+ }
99
+ /** Parse + validate a document read from disk. Throws with a human message. */
100
+ export function parseDoc(raw) {
101
+ let parsed;
102
+ try {
103
+ parsed = JSON.parse(raw);
104
+ }
105
+ catch (e) {
106
+ throw new Error(`rules file is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
107
+ }
108
+ const rulesRaw = Array.isArray(parsed)
109
+ ? parsed
110
+ : parsed && typeof parsed === 'object' && Array.isArray(parsed.rules)
111
+ ? parsed.rules
112
+ : null;
113
+ if (!rulesRaw)
114
+ throw new Error('rules file must be a JSON array or an object with a "rules" array');
115
+ const seen = new Set();
116
+ const rules = [];
117
+ for (const [i, entry] of rulesRaw.entries()) {
118
+ if (!entry || typeof entry !== 'object')
119
+ throw new Error(`rules[${i}] is not an object`);
120
+ const rule = canonicalRule(entry);
121
+ if (!rule.name)
122
+ throw new Error(`rules[${i}] is missing a non-empty "name"`);
123
+ if (seen.has(rule.name))
124
+ throw new Error(`duplicate rule name "${rule.name}" — names are the unique key`);
125
+ seen.add(rule.name);
126
+ rules.push(rule);
127
+ }
128
+ return rules;
129
+ }
130
+ /** Diff desired (file) against existing (server) by name. */
131
+ export function planApply(existing, desired, opts) {
132
+ const byName = new Map(existing.map((r) => [canonicalRule(r).name, r]));
133
+ const desiredNames = new Set(desired.map((r) => r.name));
134
+ const plan = { create: [], update: [], unchanged: [], del: [] };
135
+ for (const want of desired) {
136
+ const have = byName.get(want.name);
137
+ if (!have) {
138
+ plan.create.push(want);
139
+ continue;
140
+ }
141
+ const from = canonicalRule(have);
142
+ if (stableStringify(from) === stableStringify(want)) {
143
+ plan.unchanged.push(want);
144
+ }
145
+ else {
146
+ plan.update.push({ id: have.id, name: want.name, from, to: want });
147
+ }
148
+ }
149
+ if (opts.prune) {
150
+ for (const have of existing) {
151
+ const name = canonicalRule(have).name;
152
+ if (!desiredNames.has(name))
153
+ plan.del.push({ id: have.id, name });
154
+ }
155
+ }
156
+ return plan;
157
+ }
158
+ /** Strip a canonical rule down to the API's create/update body (drops nothing —
159
+ * the API accepts every canonical field on both POST and PATCH). */
160
+ export function toApiBody(r) {
161
+ return {
162
+ name: r.name,
163
+ description: r.description,
164
+ project_id: r.project_id,
165
+ enabled: r.enabled,
166
+ priority: r.priority,
167
+ mode: r.mode,
168
+ match: r.match,
169
+ rate_limit: r.rate_limit,
170
+ response_status: r.response_status,
171
+ response_body: r.response_body,
172
+ escalate_to_incident: r.escalate_to_incident,
173
+ };
174
+ }
175
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@tillstack/cli",
3
+ "version": "0.2.0",
4
+ "description": "TillDev command-line interface — manage TillPulse observability, TillShield edge WAF, TillGate and TillAuth from your terminal (source-map uploads, rules-as-code, and more).",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "bin": {
8
+ "tilldev": "./dist/index.js",
9
+ "tillpulse": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.0.0",
19
+ "tsx": "^4.19.2",
20
+ "typescript": "^5.7.3"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/tillafrica/tilldev.git",
25
+ "directory": "apps/cli"
26
+ },
27
+ "homepage": "https://tilldev.dev/docs/tilldev/cli",
28
+ "bugs": {
29
+ "url": "https://github.com/tillafrica/tilldev/issues"
30
+ },
31
+ "keywords": [
32
+ "tilldev",
33
+ "tillpulse",
34
+ "cli",
35
+ "observability",
36
+ "source-maps",
37
+ "tillshield",
38
+ "tillgate"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc",
45
+ "dev": "tsx src/index.ts",
46
+ "typecheck": "tsc --noEmit",
47
+ "clean": "rm -rf dist"
48
+ }
49
+ }