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.
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +268 -0
- package/src/codegen/client-codegen.js +12 -1
- package/src/codegen/codegen.js +11 -2
- package/src/codegen/security-codegen.js +408 -0
- package/src/codegen/server-codegen.js +302 -34
- package/src/parser/ast.js +21 -0
- package/src/parser/parser.js +8 -0
- package/src/parser/security-ast.js +95 -0
- package/src/parser/security-parser.js +299 -0
- package/src/version.js +1 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// Security code generator for the Tova language
|
|
2
|
+
// Produces code fragments consumed by server-codegen and client-codegen.
|
|
3
|
+
|
|
4
|
+
import { BaseCodegen } from './base-codegen.js';
|
|
5
|
+
|
|
6
|
+
export class SecurityCodegen extends BaseCodegen {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Merge all SecurityBlock nodes into a single config object.
|
|
10
|
+
* Multiple security blocks are merged (last wins on conflicts).
|
|
11
|
+
*/
|
|
12
|
+
static mergeSecurityBlocks(securityBlocks) {
|
|
13
|
+
const config = {
|
|
14
|
+
auth: null, // SecurityAuthDeclaration
|
|
15
|
+
roles: [], // Array of SecurityRoleDeclaration
|
|
16
|
+
protects: [], // Array of SecurityProtectDeclaration
|
|
17
|
+
sensitives: [], // Array of SecuritySensitiveDeclaration
|
|
18
|
+
cors: null, // SecurityCorsDeclaration
|
|
19
|
+
csp: null, // SecurityCspDeclaration
|
|
20
|
+
rateLimit: null, // SecurityRateLimitDeclaration
|
|
21
|
+
csrf: null, // SecurityCsrfDeclaration
|
|
22
|
+
audit: null, // SecurityAuditDeclaration
|
|
23
|
+
trustProxy: null, // SecurityTrustProxyDeclaration
|
|
24
|
+
hsts: null, // SecurityHstsDeclaration
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
for (const block of securityBlocks) {
|
|
28
|
+
for (const node of block.body) {
|
|
29
|
+
switch (node.type) {
|
|
30
|
+
case 'SecurityAuthDeclaration':
|
|
31
|
+
config.auth = node;
|
|
32
|
+
break;
|
|
33
|
+
case 'SecurityRoleDeclaration':
|
|
34
|
+
config.roles.push(node);
|
|
35
|
+
break;
|
|
36
|
+
case 'SecurityProtectDeclaration':
|
|
37
|
+
config.protects.push(node);
|
|
38
|
+
break;
|
|
39
|
+
case 'SecuritySensitiveDeclaration':
|
|
40
|
+
config.sensitives.push(node);
|
|
41
|
+
break;
|
|
42
|
+
case 'SecurityCorsDeclaration':
|
|
43
|
+
config.cors = node;
|
|
44
|
+
break;
|
|
45
|
+
case 'SecurityCspDeclaration':
|
|
46
|
+
config.csp = node;
|
|
47
|
+
break;
|
|
48
|
+
case 'SecurityRateLimitDeclaration':
|
|
49
|
+
config.rateLimit = node;
|
|
50
|
+
break;
|
|
51
|
+
case 'SecurityCsrfDeclaration':
|
|
52
|
+
config.csrf = node;
|
|
53
|
+
break;
|
|
54
|
+
case 'SecurityAuditDeclaration':
|
|
55
|
+
config.audit = node;
|
|
56
|
+
break;
|
|
57
|
+
case 'SecurityTrustProxyDeclaration':
|
|
58
|
+
config.trustProxy = node;
|
|
59
|
+
break;
|
|
60
|
+
case 'SecurityHstsDeclaration':
|
|
61
|
+
config.hsts = node;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return config;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate server-side security code fragments.
|
|
72
|
+
* Returns an object with code strings for each security feature.
|
|
73
|
+
*/
|
|
74
|
+
generateServerSecurity(securityConfig) {
|
|
75
|
+
const result = {
|
|
76
|
+
roleDefinitions: '',
|
|
77
|
+
authCode: '',
|
|
78
|
+
corsConfig: null, // config object for server-codegen to use
|
|
79
|
+
cspCode: '',
|
|
80
|
+
rateLimitConfig: null, // config object
|
|
81
|
+
csrfConfig: null, // config object
|
|
82
|
+
protectCode: '',
|
|
83
|
+
sensitiveCode: '',
|
|
84
|
+
auditCode: '',
|
|
85
|
+
trustProxyConfig: null, // trust_proxy value
|
|
86
|
+
hstsConfig: null, // hsts config object
|
|
87
|
+
hasAutoSanitize: false, // whether __autoSanitize was generated
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Role definitions
|
|
91
|
+
if (securityConfig.roles.length > 0) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
lines.push('// ── Security Roles ──');
|
|
94
|
+
lines.push('const __securityRoles = {');
|
|
95
|
+
for (const role of securityConfig.roles) {
|
|
96
|
+
const perms = role.permissions.map(p => JSON.stringify(p)).join(', ');
|
|
97
|
+
lines.push(` ${JSON.stringify(role.name)}: [${perms}],`);
|
|
98
|
+
}
|
|
99
|
+
lines.push('};');
|
|
100
|
+
lines.push('function __getUserRoles(user) {');
|
|
101
|
+
lines.push(' if (!user) return [];');
|
|
102
|
+
lines.push(' if (Array.isArray(user.roles)) return user.roles;');
|
|
103
|
+
lines.push(' if (user.role) return [user.role];');
|
|
104
|
+
lines.push(' return [];');
|
|
105
|
+
lines.push('}');
|
|
106
|
+
lines.push('function __hasRole(user, roleName) {');
|
|
107
|
+
lines.push(' return __getUserRoles(user).includes(roleName);');
|
|
108
|
+
lines.push('}');
|
|
109
|
+
lines.push('function __hasPermission(user, permission) {');
|
|
110
|
+
lines.push(' const userRoles = __getUserRoles(user);');
|
|
111
|
+
lines.push(' for (const r of userRoles) {');
|
|
112
|
+
lines.push(' const perms = __securityRoles[r];');
|
|
113
|
+
lines.push(' if (perms && perms.includes(permission)) return true;');
|
|
114
|
+
lines.push(' }');
|
|
115
|
+
lines.push(' return false;');
|
|
116
|
+
lines.push('}');
|
|
117
|
+
result.roleDefinitions = lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Auth config — pass through to server codegen
|
|
121
|
+
if (securityConfig.auth) {
|
|
122
|
+
const authNode = securityConfig.auth;
|
|
123
|
+
// Convert security auth config to the format server-codegen expects
|
|
124
|
+
const config = { ...authNode.config };
|
|
125
|
+
// Set the auth type as a value property (server-codegen checks .type.value)
|
|
126
|
+
config.type = { value: authNode.authType, type: 'StringLiteral' };
|
|
127
|
+
result.authConfig = config;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// CORS config — pass through to server codegen
|
|
131
|
+
if (securityConfig.cors) {
|
|
132
|
+
result.corsConfig = securityConfig.cors.config;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// CSP header generation
|
|
136
|
+
if (securityConfig.csp) {
|
|
137
|
+
const lines = [];
|
|
138
|
+
lines.push('// ── Content Security Policy ──');
|
|
139
|
+
const directives = [];
|
|
140
|
+
for (const [key, valueNode] of Object.entries(securityConfig.csp.config)) {
|
|
141
|
+
const directive = key.replace(/_/g, '-');
|
|
142
|
+
directives.push({ directive, valueNode });
|
|
143
|
+
}
|
|
144
|
+
lines.push('function __getCspHeader() {');
|
|
145
|
+
lines.push(' const parts = [];');
|
|
146
|
+
for (const { directive, valueNode } of directives) {
|
|
147
|
+
lines.push(` parts.push("${directive} " + ${this.genExpression(valueNode)}.map(v => v === "self" ? "'self'" : v === "unsafe-inline" ? "'unsafe-inline'" : v === "unsafe-eval" ? "'unsafe-eval'" : v).join(" "));`);
|
|
148
|
+
}
|
|
149
|
+
lines.push(' return parts.join("; ");');
|
|
150
|
+
lines.push('}');
|
|
151
|
+
result.cspCode = lines.join('\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Rate limit config — pass through to server codegen
|
|
155
|
+
if (securityConfig.rateLimit) {
|
|
156
|
+
result.rateLimitConfig = securityConfig.rateLimit.config;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// CSRF config
|
|
160
|
+
if (securityConfig.csrf) {
|
|
161
|
+
result.csrfConfig = securityConfig.csrf.config;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Trust proxy config
|
|
165
|
+
if (securityConfig.trustProxy) {
|
|
166
|
+
result.trustProxyConfig = securityConfig.trustProxy.value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// HSTS config
|
|
170
|
+
if (securityConfig.hsts) {
|
|
171
|
+
result.hstsConfig = securityConfig.hsts.config;
|
|
172
|
+
} else if (securityConfig.auth) {
|
|
173
|
+
// Auto-enable HSTS when auth is configured (default policy)
|
|
174
|
+
result.hstsConfig = { __autoEnabled: true };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Route protection middleware
|
|
178
|
+
if (securityConfig.protects.length > 0) {
|
|
179
|
+
const lines = [];
|
|
180
|
+
lines.push('// ── Route Protection ──');
|
|
181
|
+
lines.push('const __protectRules = [');
|
|
182
|
+
for (const protect of securityConfig.protects) {
|
|
183
|
+
const pattern = protect.pattern;
|
|
184
|
+
const requireExpr = protect.config.require;
|
|
185
|
+
let requireStr = '"authenticated"';
|
|
186
|
+
if (requireExpr) {
|
|
187
|
+
if (requireExpr.type === 'Identifier') {
|
|
188
|
+
requireStr = JSON.stringify(requireExpr.name);
|
|
189
|
+
} else {
|
|
190
|
+
requireStr = this.genExpression(requireExpr);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Convert glob-style pattern to regex
|
|
194
|
+
// 1. Replace ** with placeholder, 2. Replace * with placeholder
|
|
195
|
+
// 3. Escape all regex-special chars (including /), 4. Restore glob placeholders
|
|
196
|
+
const regexPattern = pattern
|
|
197
|
+
.replace(/\*\*/g, '\x00GLOBSTAR\x00')
|
|
198
|
+
.replace(/\*/g, '\x00STAR\x00')
|
|
199
|
+
.replace(/[.+?^${}()|[\]\\/]/g, '\\$&') // escape all regex specials including /
|
|
200
|
+
.replace(/\x00STAR\x00/g, '[^/]*') // * matches within one path segment
|
|
201
|
+
.replace(/\x00GLOBSTAR\x00/g, '.*'); // ** matches across segments
|
|
202
|
+
|
|
203
|
+
let rlMax = 'null';
|
|
204
|
+
let rlWindow = 'null';
|
|
205
|
+
if (protect.config.rate_limit) {
|
|
206
|
+
if (protect.config.rate_limit.max) {
|
|
207
|
+
rlMax = this.genExpression(protect.config.rate_limit.max);
|
|
208
|
+
}
|
|
209
|
+
if (protect.config.rate_limit.window) {
|
|
210
|
+
rlWindow = this.genExpression(protect.config.rate_limit.window);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
lines.push(` { pattern: /^${regexPattern}$/, require: ${requireStr}, rateLimit: { max: ${rlMax}, window: ${rlWindow} } },`);
|
|
215
|
+
}
|
|
216
|
+
lines.push('];');
|
|
217
|
+
lines.push('function __checkProtection(path, user) {');
|
|
218
|
+
lines.push(' for (const rule of __protectRules) {');
|
|
219
|
+
lines.push(' if (rule.pattern.test(path)) {');
|
|
220
|
+
lines.push(' if (rule.require === "authenticated") {');
|
|
221
|
+
lines.push(' if (!user) return { allowed: false, reason: "Authentication required" };');
|
|
222
|
+
lines.push(' } else {');
|
|
223
|
+
lines.push(' if (!user) return { allowed: false, reason: "Authentication required" };');
|
|
224
|
+
lines.push(' if (!__hasRole(user, rule.require)) return { allowed: false, reason: "Insufficient permissions" };');
|
|
225
|
+
lines.push(' }');
|
|
226
|
+
lines.push(' return { allowed: true, rateLimit: rule.rateLimit };');
|
|
227
|
+
lines.push(' }');
|
|
228
|
+
lines.push(' }');
|
|
229
|
+
lines.push(' return { allowed: true, rateLimit: null };');
|
|
230
|
+
lines.push('}');
|
|
231
|
+
result.protectCode = lines.join('\n');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Sensitive field sanitization
|
|
235
|
+
if (securityConfig.sensitives.length > 0) {
|
|
236
|
+
const lines = [];
|
|
237
|
+
lines.push('// ── Sensitive Field Sanitization ──');
|
|
238
|
+
|
|
239
|
+
// Identity comparison helper for visible_to: ["self"]
|
|
240
|
+
// Checks multiple common identity fields instead of hardcoded user.id === obj.id
|
|
241
|
+
const hasVisibleTo = securityConfig.sensitives.some(s => s.config.visible_to);
|
|
242
|
+
if (hasVisibleTo) {
|
|
243
|
+
lines.push('function __isSameIdentity(user, obj) {');
|
|
244
|
+
lines.push(' const __idFields = ["id", "_id", "userId", "user_id", "uuid"];');
|
|
245
|
+
lines.push(' for (const f of __idFields) {');
|
|
246
|
+
lines.push(' if (user[f] != null && obj[f] != null && user[f] === obj[f]) return true;');
|
|
247
|
+
lines.push(' }');
|
|
248
|
+
lines.push(' return false;');
|
|
249
|
+
lines.push('}');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Group by type
|
|
253
|
+
const byType = {};
|
|
254
|
+
for (const s of securityConfig.sensitives) {
|
|
255
|
+
if (!byType[s.typeName]) byType[s.typeName] = [];
|
|
256
|
+
byType[s.typeName].push(s);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const [typeName, fields] of Object.entries(byType)) {
|
|
260
|
+
const fnName = `__sanitize${typeName}`;
|
|
261
|
+
lines.push(`function ${fnName}(obj, user) {`);
|
|
262
|
+
lines.push(' if (!obj) return obj;');
|
|
263
|
+
lines.push(' const result = { ...obj };');
|
|
264
|
+
for (const field of fields) {
|
|
265
|
+
if (field.config.never_expose) {
|
|
266
|
+
lines.push(` delete result.${field.fieldName};`);
|
|
267
|
+
} else if (field.config.visible_to) {
|
|
268
|
+
const visibleExpr = this.genExpression(field.config.visible_to);
|
|
269
|
+
lines.push(` const __visibleTo = ${visibleExpr};`);
|
|
270
|
+
lines.push(` const __canSee = __visibleTo.some(v => v === "self" ? (user && __isSameIdentity(user, obj)) : __hasRole(user, v));`);
|
|
271
|
+
lines.push(` if (!__canSee) delete result.${field.fieldName};`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
lines.push(' return result;');
|
|
275
|
+
lines.push('}');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Fix 6a: Auto-sanitize dispatcher
|
|
279
|
+
const typeNames = Object.keys(byType);
|
|
280
|
+
lines.push('function __autoSanitize(data, user) {');
|
|
281
|
+
lines.push(' if (data == null) return data;');
|
|
282
|
+
lines.push(' if (Array.isArray(data)) return data.map(item => __autoSanitize(item, user));');
|
|
283
|
+
lines.push(' if (typeof data !== "object") return data;');
|
|
284
|
+
lines.push(' const __typeName = data.__type || data.__tag || (data.constructor && data.constructor.name !== "Object" ? data.constructor.name : null);');
|
|
285
|
+
for (const typeName of typeNames) {
|
|
286
|
+
lines.push(` if (__typeName === ${JSON.stringify(typeName)}) return __sanitize${typeName}(data, user);`);
|
|
287
|
+
}
|
|
288
|
+
// Recurse into nested objects
|
|
289
|
+
lines.push(' const __out = {};');
|
|
290
|
+
lines.push(' for (const [k, v] of Object.entries(data)) {');
|
|
291
|
+
lines.push(' __out[k] = __autoSanitize(v, user);');
|
|
292
|
+
lines.push(' }');
|
|
293
|
+
lines.push(' return __out;');
|
|
294
|
+
lines.push('}');
|
|
295
|
+
|
|
296
|
+
result.sensitiveCode = lines.join('\n');
|
|
297
|
+
result.hasAutoSanitize = true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Audit logging
|
|
301
|
+
if (securityConfig.audit) {
|
|
302
|
+
const lines = [];
|
|
303
|
+
lines.push('// ── Audit Logging ──');
|
|
304
|
+
const storeExpr = securityConfig.audit.config.store
|
|
305
|
+
? this.genExpression(securityConfig.audit.config.store)
|
|
306
|
+
: '"audit_log"';
|
|
307
|
+
const retainExpr = securityConfig.audit.config.retain
|
|
308
|
+
? this.genExpression(securityConfig.audit.config.retain)
|
|
309
|
+
: '90';
|
|
310
|
+
lines.push(`const __auditStore = ${storeExpr};`);
|
|
311
|
+
lines.push('if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(__auditStore)) throw new Error("Invalid audit store table name: " + __auditStore);');
|
|
312
|
+
lines.push(`const __auditRetainDays = ${retainExpr};`);
|
|
313
|
+
|
|
314
|
+
if (securityConfig.audit.config.events) {
|
|
315
|
+
lines.push(`const __auditEvents = ${this.genExpression(securityConfig.audit.config.events)};`);
|
|
316
|
+
} else {
|
|
317
|
+
lines.push('const __auditEvents = [];');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
lines.push('async function __auditLog(event, details, user) {');
|
|
321
|
+
lines.push(' const entry = {');
|
|
322
|
+
lines.push(' event,');
|
|
323
|
+
lines.push(' timestamp: new Date().toISOString(),');
|
|
324
|
+
lines.push(' user: user ? { id: user.id, roles: __getUserRoles ? __getUserRoles(user) : (user.roles || [user.role].filter(Boolean)) } : null,');
|
|
325
|
+
lines.push(' details,');
|
|
326
|
+
lines.push(' };');
|
|
327
|
+
lines.push(' if (typeof db !== "undefined" && db.run) {');
|
|
328
|
+
lines.push(` try { await db.run("INSERT INTO " + __auditStore + " (event, timestamp, user_id, details) VALUES (?, ?, ?, ?)", entry.event, entry.timestamp, entry.user ? entry.user.id : null, JSON.stringify(entry.details)); } catch (__auditErr) { console.error("[tova:audit] Failed to write audit log:", __auditErr.message || __auditErr); }`);
|
|
329
|
+
lines.push(' }');
|
|
330
|
+
lines.push('}');
|
|
331
|
+
result.auditCode = lines.join('\n');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Generate client-side security code fragments.
|
|
339
|
+
*/
|
|
340
|
+
generateClientSecurity(securityConfig) {
|
|
341
|
+
const lines = [];
|
|
342
|
+
|
|
343
|
+
// Auth token injection for RPC proxy
|
|
344
|
+
if (securityConfig.auth) {
|
|
345
|
+
// Check if auth storage is "cookie" (HttpOnly cookie mode)
|
|
346
|
+
const storageNode = securityConfig.auth.config.storage;
|
|
347
|
+
const isCookieAuth = storageNode && storageNode.type === 'StringLiteral' && storageNode.value === 'cookie';
|
|
348
|
+
|
|
349
|
+
if (isCookieAuth) {
|
|
350
|
+
// HttpOnly cookie mode: server manages tokens via Set-Cookie
|
|
351
|
+
// Client just ensures credentials are included in fetch
|
|
352
|
+
lines.push('// ── Security: Auth Token (HttpOnly Cookie) ──');
|
|
353
|
+
lines.push('function getAuthToken() { return null; /* managed by HttpOnly cookie */ }');
|
|
354
|
+
lines.push('function setAuthToken(_token) { /* no-op: server sets HttpOnly cookie */ }');
|
|
355
|
+
lines.push('function clearAuthToken() {');
|
|
356
|
+
lines.push(' fetch("/rpc/__logout", { method: "POST", credentials: "include" }).catch(() => {});');
|
|
357
|
+
lines.push('}');
|
|
358
|
+
lines.push('configureRPC({ credentials: "include" });');
|
|
359
|
+
lines.push('');
|
|
360
|
+
} else {
|
|
361
|
+
// localStorage mode (default)
|
|
362
|
+
lines.push('// ── Security: Auth Token ──');
|
|
363
|
+
lines.push('function getAuthToken() {');
|
|
364
|
+
lines.push(' return localStorage.getItem("__tova_auth_token");');
|
|
365
|
+
lines.push('}');
|
|
366
|
+
lines.push('function setAuthToken(token) {');
|
|
367
|
+
lines.push(' localStorage.setItem("__tova_auth_token", token);');
|
|
368
|
+
lines.push('}');
|
|
369
|
+
lines.push('function clearAuthToken() {');
|
|
370
|
+
lines.push(' localStorage.removeItem("__tova_auth_token");');
|
|
371
|
+
lines.push('}');
|
|
372
|
+
lines.push('addRPCInterceptor({');
|
|
373
|
+
lines.push(' request({ options }) {');
|
|
374
|
+
lines.push(' const token = getAuthToken();');
|
|
375
|
+
lines.push(' if (token) options.headers["Authorization"] = "Bearer " + token;');
|
|
376
|
+
lines.push(' return options;');
|
|
377
|
+
lines.push(' }');
|
|
378
|
+
lines.push('});');
|
|
379
|
+
lines.push('');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Role definitions and can() helper (Fix 8: advisory comment)
|
|
384
|
+
if (securityConfig.roles.length > 0) {
|
|
385
|
+
lines.push('// ── Security: Roles ──');
|
|
386
|
+
lines.push('// NOTE: Client-side role checking is for UI purposes only. All authorization is enforced server-side.');
|
|
387
|
+
lines.push('const __clientRoles = {');
|
|
388
|
+
for (const role of securityConfig.roles) {
|
|
389
|
+
const perms = role.permissions.map(p => JSON.stringify(p)).join(', ');
|
|
390
|
+
lines.push(` ${JSON.stringify(role.name)}: [${perms}],`);
|
|
391
|
+
}
|
|
392
|
+
lines.push('};');
|
|
393
|
+
lines.push('let __currentUserRoles = [];');
|
|
394
|
+
lines.push('function setUserRole(role) { __currentUserRoles = Array.isArray(role) ? role : [role]; }');
|
|
395
|
+
lines.push('function getUserRole() { return __currentUserRoles; }');
|
|
396
|
+
lines.push('function can(permission) {');
|
|
397
|
+
lines.push(' for (const r of __currentUserRoles) {');
|
|
398
|
+
lines.push(' const perms = __clientRoles[r];');
|
|
399
|
+
lines.push(' if (perms && perms.includes(permission)) return true;');
|
|
400
|
+
lines.push(' }');
|
|
401
|
+
lines.push(' return false;');
|
|
402
|
+
lines.push('}');
|
|
403
|
+
lines.push('');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return lines.join('\n');
|
|
407
|
+
}
|
|
408
|
+
}
|