tova 0.4.7 → 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.
@@ -0,0 +1,95 @@
1
+ // Security-specific AST Node definitions for the Tova language
2
+ // Extracted for lazy loading — only loaded when security { } blocks are used.
3
+
4
+ export class SecurityAuthDeclaration {
5
+ constructor(authType, config, loc) {
6
+ this.type = 'SecurityAuthDeclaration';
7
+ this.authType = authType; // "jwt" or "api_key"
8
+ this.config = config; // { secret, expires, ... }
9
+ this.loc = loc;
10
+ }
11
+ }
12
+
13
+ export class SecurityRoleDeclaration {
14
+ constructor(name, permissions, loc) {
15
+ this.type = 'SecurityRoleDeclaration';
16
+ this.name = name; // string — role name, e.g. "Admin"
17
+ this.permissions = permissions; // Array of strings — permission names
18
+ this.loc = loc;
19
+ }
20
+ }
21
+
22
+ export class SecurityProtectDeclaration {
23
+ constructor(pattern, config, loc) {
24
+ this.type = 'SecurityProtectDeclaration';
25
+ this.pattern = pattern; // string — route pattern, e.g. "/api/admin/*"
26
+ this.config = config; // { require, rate_limit: { max, window } }
27
+ this.loc = loc;
28
+ }
29
+ }
30
+
31
+ export class SecuritySensitiveDeclaration {
32
+ constructor(typeName, fieldName, config, loc) {
33
+ this.type = 'SecuritySensitiveDeclaration';
34
+ this.typeName = typeName; // string — type name, e.g. "User"
35
+ this.fieldName = fieldName; // string — field name, e.g. "password"
36
+ this.config = config; // { hash, never_expose, visible_to }
37
+ this.loc = loc;
38
+ }
39
+ }
40
+
41
+ export class SecurityCorsDeclaration {
42
+ constructor(config, loc) {
43
+ this.type = 'SecurityCorsDeclaration';
44
+ this.config = config; // { origins, methods, credentials }
45
+ this.loc = loc;
46
+ }
47
+ }
48
+
49
+ export class SecurityCspDeclaration {
50
+ constructor(config, loc) {
51
+ this.type = 'SecurityCspDeclaration';
52
+ this.config = config; // { default_src, script_src, style_src, ... }
53
+ this.loc = loc;
54
+ }
55
+ }
56
+
57
+ export class SecurityRateLimitDeclaration {
58
+ constructor(config, loc) {
59
+ this.type = 'SecurityRateLimitDeclaration';
60
+ this.config = config; // { max, window }
61
+ this.loc = loc;
62
+ }
63
+ }
64
+
65
+ export class SecurityCsrfDeclaration {
66
+ constructor(config, loc) {
67
+ this.type = 'SecurityCsrfDeclaration';
68
+ this.config = config; // { enabled, exempt }
69
+ this.loc = loc;
70
+ }
71
+ }
72
+
73
+ export class SecurityAuditDeclaration {
74
+ constructor(config, loc) {
75
+ this.type = 'SecurityAuditDeclaration';
76
+ this.config = config; // { events, store, retain }
77
+ this.loc = loc;
78
+ }
79
+ }
80
+
81
+ export class SecurityTrustProxyDeclaration {
82
+ constructor(value, loc) {
83
+ this.type = 'SecurityTrustProxyDeclaration';
84
+ this.value = value; // true | false | "loopback"
85
+ this.loc = loc;
86
+ }
87
+ }
88
+
89
+ export class SecurityHstsDeclaration {
90
+ constructor(config, loc) {
91
+ this.type = 'SecurityHstsDeclaration';
92
+ this.config = config; // { enabled, max_age, include_subdomains, preload }
93
+ this.loc = loc;
94
+ }
95
+ }
@@ -0,0 +1,299 @@
1
+ // Security-specific parser methods for the Tova language
2
+ // Extracted from parser.js for lazy loading — only loaded when security { } blocks are encountered.
3
+
4
+ import { TokenType } from '../lexer/tokens.js';
5
+ import * as AST from './ast.js';
6
+
7
+ // Keywords that may appear as config keys inside security blocks
8
+ const CONFIG_KEY_TOKENS = new Set([
9
+ TokenType.IDENTIFIER, TokenType.TYPE, TokenType.STORE,
10
+ TokenType.FN, TokenType.MATCH, TokenType.IF,
11
+ ]);
12
+
13
+ export function installSecurityParser(ParserClass) {
14
+ if (ParserClass.prototype._securityParserInstalled) return;
15
+ ParserClass.prototype._securityParserInstalled = true;
16
+
17
+ // Helper: read a config key (identifier or keyword that acts as identifier)
18
+ ParserClass.prototype._expectSecurityConfigKey = function(context) {
19
+ if (CONFIG_KEY_TOKENS.has(this.current().type)) {
20
+ return this.advance().value;
21
+ }
22
+ this.error(`Expected ${context} config key`);
23
+ };
24
+
25
+ ParserClass.prototype.parseSecurityBlock = function() {
26
+ const l = this.loc();
27
+ this.advance(); // consume 'security'
28
+ this.expect(TokenType.LBRACE, "Expected '{' after 'security'");
29
+ const body = [];
30
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
31
+ try {
32
+ const stmt = this.parseSecurityStatement();
33
+ if (stmt) body.push(stmt);
34
+ } catch (e) {
35
+ this.errors.push(e);
36
+ this._synchronizeBlock();
37
+ }
38
+ }
39
+ this.expect(TokenType.RBRACE, "Expected '}' to close security block");
40
+ return new AST.SecurityBlock(body, l);
41
+ };
42
+
43
+ ParserClass.prototype.parseSecurityStatement = function() {
44
+ if (this.check(TokenType.IDENTIFIER)) {
45
+ const val = this.current().value;
46
+
47
+ if (val === 'auth' && (this.peek(1).type === TokenType.IDENTIFIER || this.peek(1).type === TokenType.LBRACE)) {
48
+ return this.parseSecurityAuth();
49
+ }
50
+ if (val === 'role' && this.peek(1).type === TokenType.IDENTIFIER) {
51
+ return this.parseSecurityRole();
52
+ }
53
+ if (val === 'protect' && this.peek(1).type === TokenType.STRING) {
54
+ return this.parseSecurityProtect();
55
+ }
56
+ if (val === 'sensitive' && this.peek(1).type === TokenType.IDENTIFIER) {
57
+ return this.parseSecuritySensitive();
58
+ }
59
+ if (val === 'cors' && this.peek(1).type === TokenType.LBRACE) {
60
+ return this.parseSecurityCors();
61
+ }
62
+ if (val === 'csp' && this.peek(1).type === TokenType.LBRACE) {
63
+ return this.parseSecurityCsp();
64
+ }
65
+ if (val === 'rate_limit' && this.peek(1).type === TokenType.LBRACE) {
66
+ return this.parseSecurityRateLimit();
67
+ }
68
+ if (val === 'csrf' && this.peek(1).type === TokenType.LBRACE) {
69
+ return this.parseSecurityCsrf();
70
+ }
71
+ if (val === 'audit' && this.peek(1).type === TokenType.LBRACE) {
72
+ return this.parseSecurityAudit();
73
+ }
74
+ if (val === 'trust_proxy') {
75
+ return this.parseSecurityTrustProxy();
76
+ }
77
+ if (val === 'hsts' && this.peek(1).type === TokenType.LBRACE) {
78
+ return this.parseSecurityHsts();
79
+ }
80
+ }
81
+
82
+ this.error("Expected security declaration (auth, role, protect, sensitive, cors, csp, rate_limit, csrf, audit, trust_proxy, hsts)");
83
+ };
84
+
85
+ // auth jwt { secret: ..., expires: ... }
86
+ ParserClass.prototype.parseSecurityAuth = function() {
87
+ const l = this.loc();
88
+ this.advance(); // consume 'auth'
89
+ let authType = 'jwt';
90
+ if (this.check(TokenType.IDENTIFIER)) {
91
+ authType = this.advance().value;
92
+ }
93
+ this.expect(TokenType.LBRACE, "Expected '{' after auth type");
94
+ const config = {};
95
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
96
+ const key = this._expectSecurityConfigKey("auth");
97
+ this.expect(TokenType.COLON, "Expected ':' after auth key");
98
+ const value = this.parseExpression();
99
+ config[key] = value;
100
+ this.match(TokenType.COMMA);
101
+ }
102
+ this.expect(TokenType.RBRACE, "Expected '}' to close auth config");
103
+ return new AST.SecurityAuthDeclaration(authType, config, l);
104
+ };
105
+
106
+ // role Admin { can: [manage_users, view_analytics] }
107
+ ParserClass.prototype.parseSecurityRole = function() {
108
+ const l = this.loc();
109
+ this.advance(); // consume 'role'
110
+ const name = this.expect(TokenType.IDENTIFIER, "Expected role name").value;
111
+ this.expect(TokenType.LBRACE, "Expected '{' after role name");
112
+ const permissions = [];
113
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
114
+ const key = this._expectSecurityConfigKey("role");
115
+ this.expect(TokenType.COLON, "Expected ':' after role key");
116
+ if (key === 'can') {
117
+ // Parse array of identifiers: [manage_users, view_analytics]
118
+ this.expect(TokenType.LBRACKET, "Expected '[' for permissions list");
119
+ while (!this.check(TokenType.RBRACKET) && !this.isAtEnd()) {
120
+ const perm = this.expect(TokenType.IDENTIFIER, "Expected permission name").value;
121
+ permissions.push(perm);
122
+ this.match(TokenType.COMMA);
123
+ }
124
+ this.expect(TokenType.RBRACKET, "Expected ']' to close permissions list");
125
+ } else {
126
+ // Skip unknown keys
127
+ this.parseExpression();
128
+ }
129
+ this.match(TokenType.COMMA);
130
+ }
131
+ this.expect(TokenType.RBRACE, "Expected '}' to close role definition");
132
+ return new AST.SecurityRoleDeclaration(name, permissions, l);
133
+ };
134
+
135
+ // protect "/api/admin/*" { require: Admin, rate_limit: { max: 100, window: 60 } }
136
+ ParserClass.prototype.parseSecurityProtect = function() {
137
+ const l = this.loc();
138
+ this.advance(); // consume 'protect'
139
+ const pattern = this.expect(TokenType.STRING, "Expected route pattern string").value;
140
+ this.expect(TokenType.LBRACE, "Expected '{' after protect pattern");
141
+ const config = {};
142
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
143
+ const key = this._expectSecurityConfigKey("protect");
144
+ this.expect(TokenType.COLON, "Expected ':' after protect key");
145
+ if (key === 'rate_limit') {
146
+ // Nested config: { max: 100, window: 60 }
147
+ this.expect(TokenType.LBRACE, "Expected '{' for rate_limit config");
148
+ const rlConfig = {};
149
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
150
+ const rlKey = this._expectSecurityConfigKey("rate_limit");
151
+ this.expect(TokenType.COLON, "Expected ':' after rate_limit key");
152
+ rlConfig[rlKey] = this.parseExpression();
153
+ this.match(TokenType.COMMA);
154
+ }
155
+ this.expect(TokenType.RBRACE, "Expected '}' to close rate_limit config");
156
+ config[key] = rlConfig;
157
+ } else {
158
+ config[key] = this.parseExpression();
159
+ }
160
+ this.match(TokenType.COMMA);
161
+ }
162
+ this.expect(TokenType.RBRACE, "Expected '}' to close protect config");
163
+ return new AST.SecurityProtectDeclaration(pattern, config, l);
164
+ };
165
+
166
+ // sensitive User.password { hash: "bcrypt", never_expose: true }
167
+ ParserClass.prototype.parseSecuritySensitive = function() {
168
+ const l = this.loc();
169
+ this.advance(); // consume 'sensitive'
170
+ const typeName = this.expect(TokenType.IDENTIFIER, "Expected type name").value;
171
+ this.expect(TokenType.DOT, "Expected '.' after type name in sensitive declaration");
172
+ const fieldName = this.expect(TokenType.IDENTIFIER, "Expected field name").value;
173
+ this.expect(TokenType.LBRACE, "Expected '{' after sensitive field");
174
+ const config = {};
175
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
176
+ const key = this._expectSecurityConfigKey("sensitive");
177
+ this.expect(TokenType.COLON, "Expected ':' after sensitive key");
178
+ config[key] = this.parseExpression();
179
+ this.match(TokenType.COMMA);
180
+ }
181
+ this.expect(TokenType.RBRACE, "Expected '}' to close sensitive config");
182
+ return new AST.SecuritySensitiveDeclaration(typeName, fieldName, config, l);
183
+ };
184
+
185
+ // cors { origins: ["..."], methods: [GET, POST], credentials: true }
186
+ ParserClass.prototype.parseSecurityCors = function() {
187
+ const l = this.loc();
188
+ this.advance(); // consume 'cors'
189
+ this.expect(TokenType.LBRACE, "Expected '{' after 'cors'");
190
+ const config = {};
191
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
192
+ const key = this._expectSecurityConfigKey("cors");
193
+ this.expect(TokenType.COLON, "Expected ':' after cors key");
194
+ config[key] = this.parseExpression();
195
+ this.match(TokenType.COMMA);
196
+ }
197
+ this.expect(TokenType.RBRACE, "Expected '}' to close cors config");
198
+ return new AST.SecurityCorsDeclaration(config, l);
199
+ };
200
+
201
+ // csp { default_src: ["self"], script_src: ["self"] }
202
+ ParserClass.prototype.parseSecurityCsp = function() {
203
+ const l = this.loc();
204
+ this.advance(); // consume 'csp'
205
+ this.expect(TokenType.LBRACE, "Expected '{' after 'csp'");
206
+ const config = {};
207
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
208
+ const key = this._expectSecurityConfigKey("csp");
209
+ this.expect(TokenType.COLON, "Expected ':' after csp key");
210
+ config[key] = this.parseExpression();
211
+ this.match(TokenType.COMMA);
212
+ }
213
+ this.expect(TokenType.RBRACE, "Expected '}' to close csp config");
214
+ return new AST.SecurityCspDeclaration(config, l);
215
+ };
216
+
217
+ // rate_limit { max: 1000, window: 3600 }
218
+ ParserClass.prototype.parseSecurityRateLimit = function() {
219
+ const l = this.loc();
220
+ this.advance(); // consume 'rate_limit'
221
+ this.expect(TokenType.LBRACE, "Expected '{' after 'rate_limit'");
222
+ const config = {};
223
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
224
+ const key = this._expectSecurityConfigKey("rate_limit");
225
+ this.expect(TokenType.COLON, "Expected ':' after rate_limit key");
226
+ config[key] = this.parseExpression();
227
+ this.match(TokenType.COMMA);
228
+ }
229
+ this.expect(TokenType.RBRACE, "Expected '}' to close rate_limit config");
230
+ return new AST.SecurityRateLimitDeclaration(config, l);
231
+ };
232
+
233
+ // csrf { enabled: true, exempt: ["/api/webhooks/*"] }
234
+ ParserClass.prototype.parseSecurityCsrf = function() {
235
+ const l = this.loc();
236
+ this.advance(); // consume 'csrf'
237
+ this.expect(TokenType.LBRACE, "Expected '{' after 'csrf'");
238
+ const config = {};
239
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
240
+ const key = this._expectSecurityConfigKey("csrf");
241
+ this.expect(TokenType.COLON, "Expected ':' after csrf key");
242
+ config[key] = this.parseExpression();
243
+ this.match(TokenType.COMMA);
244
+ }
245
+ this.expect(TokenType.RBRACE, "Expected '}' to close csrf config");
246
+ return new AST.SecurityCsrfDeclaration(config, l);
247
+ };
248
+
249
+ // trust_proxy true | trust_proxy false | trust_proxy "loopback"
250
+ ParserClass.prototype.parseSecurityTrustProxy = function() {
251
+ const l = this.loc();
252
+ this.advance(); // consume 'trust_proxy'
253
+ // Expect a value: true, false, or "loopback"
254
+ const valueToken = this.advance();
255
+ let value = false;
256
+ if (valueToken.type === TokenType.STRING) {
257
+ value = valueToken.value;
258
+ } else if (valueToken.value === 'true') {
259
+ value = true;
260
+ } else if (valueToken.value === 'false') {
261
+ value = false;
262
+ } else {
263
+ this.error('Expected true, false, or string for trust_proxy');
264
+ }
265
+ return new AST.SecurityTrustProxyDeclaration(value, l);
266
+ };
267
+
268
+ // hsts { max_age: 31536000, include_subdomains: true, preload: false }
269
+ ParserClass.prototype.parseSecurityHsts = function() {
270
+ const l = this.loc();
271
+ this.advance(); // consume 'hsts'
272
+ this.expect(TokenType.LBRACE, "Expected '{' after 'hsts'");
273
+ const config = {};
274
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
275
+ const key = this._expectSecurityConfigKey("hsts");
276
+ this.expect(TokenType.COLON, "Expected ':' after hsts key");
277
+ config[key] = this.parseExpression();
278
+ this.match(TokenType.COMMA);
279
+ }
280
+ this.expect(TokenType.RBRACE, "Expected '}' to close hsts config");
281
+ return new AST.SecurityHstsDeclaration(config, l);
282
+ };
283
+
284
+ // audit { events: [login, logout], store: "audit_log", retain: 90 }
285
+ ParserClass.prototype.parseSecurityAudit = function() {
286
+ const l = this.loc();
287
+ this.advance(); // consume 'audit'
288
+ this.expect(TokenType.LBRACE, "Expected '{' after 'audit'");
289
+ const config = {};
290
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
291
+ const key = this._expectSecurityConfigKey("audit");
292
+ this.expect(TokenType.COLON, "Expected ':' after audit key");
293
+ config[key] = this.parseExpression();
294
+ this.match(TokenType.COMMA);
295
+ }
296
+ this.expect(TokenType.RBRACE, "Expected '}' to close audit config");
297
+ return new AST.SecurityAuditDeclaration(config, l);
298
+ };
299
+ }
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/embed-runtime.js — do not edit
2
- export const VERSION = "0.4.7";
2
+ export const VERSION = "0.5.0";