tjs-lang 0.7.6 → 0.7.8
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/CLAUDE.md +101 -26
- package/bin/docs.js +4 -1
- package/demo/docs.json +46 -12
- package/demo/src/examples.test.ts +1 -0
- package/demo/src/imports.test.ts +16 -4
- package/demo/src/imports.ts +60 -15
- package/demo/src/playground-shared.ts +9 -8
- package/demo/src/tfs-worker.js +205 -147
- package/demo/src/tjs-playground.ts +34 -10
- package/demo/src/ts-playground.ts +24 -8
- package/dist/index.js +140 -119
- package/dist/index.js.map +4 -4
- package/dist/src/lang/bool-coercion.d.ts +50 -0
- package/dist/src/lang/docs.d.ts +31 -6
- package/dist/src/lang/linter.d.ts +8 -0
- package/dist/src/lang/parser-transforms.d.ts +18 -0
- package/dist/src/lang/parser-types.d.ts +2 -0
- package/dist/src/lang/parser.d.ts +9 -0
- package/dist/src/lang/runtime.d.ts +34 -0
- package/dist/src/lang/types.d.ts +9 -1
- package/dist/src/rbac/index.d.ts +1 -1
- package/dist/src/vm/runtime.d.ts +1 -1
- package/dist/tjs-eval.js +44 -39
- package/dist/tjs-eval.js.map +4 -4
- package/dist/tjs-from-ts.js +20 -20
- package/dist/tjs-from-ts.js.map +3 -3
- package/dist/tjs-lang.js +86 -80
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +50 -45
- package/dist/tjs-vm.js.map +4 -4
- package/llms.txt +79 -0
- package/package.json +3 -2
- package/src/cli/commands/convert.test.ts +16 -21
- package/src/lang/bool-coercion.test.ts +203 -0
- package/src/lang/bool-coercion.ts +314 -0
- package/src/lang/codegen.test.ts +177 -0
- package/src/lang/docs.test.ts +328 -1
- package/src/lang/docs.ts +424 -24
- package/src/lang/emitters/ast.ts +11 -12
- package/src/lang/emitters/dts.test.ts +41 -0
- package/src/lang/emitters/dts.ts +9 -0
- package/src/lang/emitters/js-tests.ts +16 -4
- package/src/lang/emitters/js.ts +208 -2
- package/src/lang/features.test.ts +22 -0
- package/src/lang/inference.ts +54 -0
- package/src/lang/linter.test.ts +104 -1
- package/src/lang/linter.ts +124 -1
- package/src/lang/parser-params.ts +31 -0
- package/src/lang/parser-transforms.ts +539 -6
- package/src/lang/parser-types.ts +2 -0
- package/src/lang/parser.test.ts +73 -1
- package/src/lang/parser.ts +85 -1
- package/src/lang/runtime.ts +98 -0
- package/src/lang/tests.ts +21 -8
- package/src/lang/types.ts +6 -0
- package/src/rbac/index.ts +2 -2
- package/src/rbac/rules.tjs.d.ts +9 -0
- package/src/vm/atoms/batteries.ts +2 -2
- package/src/vm/runtime.ts +10 -3
- package/dist/src/rbac/rules.d.ts +0 -184
- package/src/rbac/rules.js +0 -338
package/src/rbac/rules.js
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
/*#
|
|
2
|
-
# RBAC Security Rules (Pure Logic)
|
|
3
|
-
|
|
4
|
-
Pure security rule evaluation logic, independent of storage backend.
|
|
5
|
-
This module contains no I/O - it only evaluates rules against contexts.
|
|
6
|
-
|
|
7
|
-
## Rule Context
|
|
8
|
-
- `_uid` - authenticated user ID (null if public)
|
|
9
|
-
- `_roles` - array of user roles
|
|
10
|
-
- `_method` - 'read' | 'write' | 'delete'
|
|
11
|
-
- `_collection` - collection name
|
|
12
|
-
- `_docId` - document ID
|
|
13
|
-
- `doc` - existing document data (for read/write/delete)
|
|
14
|
-
- `newData` - incoming data (for write only)
|
|
15
|
-
|
|
16
|
-
## Rule Response
|
|
17
|
-
- Return `true` to allow
|
|
18
|
-
- Return `false` to deny
|
|
19
|
-
- Return `{ allow: true/false, reason: string }` for detailed response
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/*#
|
|
23
|
-
## Access Rule Shortcuts
|
|
24
|
-
|
|
25
|
-
Evaluates simple access rule strings without AJS overhead.
|
|
26
|
-
Returns { allowed: boolean, reason?: string } or null if not a shortcut.
|
|
27
|
-
|
|
28
|
-
Shortcuts:
|
|
29
|
-
- 'none' - deny all
|
|
30
|
-
- 'all' - allow all
|
|
31
|
-
- 'authenticated' - must be logged in
|
|
32
|
-
- 'admin' - must have admin role
|
|
33
|
-
- 'author' - must have author role
|
|
34
|
-
- 'owner:fieldName' - doc[fieldName] === _uid
|
|
35
|
-
- 'role:roleName' - _roles.includes(roleName)
|
|
36
|
-
*/
|
|
37
|
-
export function evaluateAccessShortcut(accessRule, context) {
|
|
38
|
-
if (typeof accessRule !== 'string') return null
|
|
39
|
-
|
|
40
|
-
const { _uid, _roles, doc, newData } = context
|
|
41
|
-
|
|
42
|
-
switch (accessRule) {
|
|
43
|
-
case 'none':
|
|
44
|
-
return { allowed: false, reason: 'Access denied' }
|
|
45
|
-
|
|
46
|
-
case 'all':
|
|
47
|
-
return { allowed: true }
|
|
48
|
-
|
|
49
|
-
case 'authenticated':
|
|
50
|
-
return _uid
|
|
51
|
-
? { allowed: true }
|
|
52
|
-
: { allowed: false, reason: 'Authentication required' }
|
|
53
|
-
|
|
54
|
-
case 'admin':
|
|
55
|
-
return _roles?.includes('admin')
|
|
56
|
-
? { allowed: true }
|
|
57
|
-
: { allowed: false, reason: 'Admin role required' }
|
|
58
|
-
|
|
59
|
-
case 'author':
|
|
60
|
-
return _roles?.includes('author')
|
|
61
|
-
? { allowed: true }
|
|
62
|
-
: { allowed: false, reason: 'Author role required' }
|
|
63
|
-
|
|
64
|
-
default:
|
|
65
|
-
// owner:fieldName pattern
|
|
66
|
-
if (accessRule.startsWith('owner:')) {
|
|
67
|
-
const field = accessRule.slice(6)
|
|
68
|
-
const checkDoc = doc || newData
|
|
69
|
-
if (!_uid) {
|
|
70
|
-
return { allowed: false, reason: 'Authentication required' }
|
|
71
|
-
}
|
|
72
|
-
if (checkDoc && checkDoc[field] === _uid) {
|
|
73
|
-
return { allowed: true }
|
|
74
|
-
}
|
|
75
|
-
if (!doc && newData && newData[field] === _uid) {
|
|
76
|
-
return { allowed: true }
|
|
77
|
-
}
|
|
78
|
-
return { allowed: false, reason: `Must be owner (${field})` }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// role:roleName pattern
|
|
82
|
-
if (accessRule.startsWith('role:')) {
|
|
83
|
-
const role = accessRule.slice(5)
|
|
84
|
-
return _roles?.includes(role)
|
|
85
|
-
? { allowed: true }
|
|
86
|
-
: { allowed: false, reason: `Role '${role}' required` }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return null // Not a recognized shortcut
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
evaluateAccessShortcut.__tjs = {
|
|
93
|
-
params: {
|
|
94
|
-
accessRule: {
|
|
95
|
-
type: {
|
|
96
|
-
kind: 'any',
|
|
97
|
-
},
|
|
98
|
-
required: false,
|
|
99
|
-
},
|
|
100
|
-
context: {
|
|
101
|
-
type: {
|
|
102
|
-
kind: 'any',
|
|
103
|
-
},
|
|
104
|
-
required: false,
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
unsafe: true,
|
|
108
|
-
source: 'rules.tjs:37',
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/*#
|
|
112
|
-
## Select Access Rule
|
|
113
|
-
|
|
114
|
-
Determines which access rule to use based on the operation method.
|
|
115
|
-
Supports granular rules (read/create/update/delete) with fallbacks.
|
|
116
|
-
*/
|
|
117
|
-
export function selectAccessRule(rule, context) {
|
|
118
|
-
const { _method, doc } = context
|
|
119
|
-
|
|
120
|
-
// Method-specific rules with fallbacks
|
|
121
|
-
if (_method === 'read' && rule.read !== undefined) {
|
|
122
|
-
return rule.read
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (_method === 'write') {
|
|
126
|
-
// Distinguish create vs update
|
|
127
|
-
if (!doc && rule.create !== undefined) {
|
|
128
|
-
return rule.create
|
|
129
|
-
}
|
|
130
|
-
if (doc && rule.update !== undefined) {
|
|
131
|
-
return rule.update
|
|
132
|
-
}
|
|
133
|
-
if (rule.write !== undefined) {
|
|
134
|
-
return rule.write
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (_method === 'delete' && rule.delete !== undefined) {
|
|
139
|
-
return rule.delete
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Fall back to code for backwards compatibility
|
|
143
|
-
return rule.code
|
|
144
|
-
}
|
|
145
|
-
selectAccessRule.__tjs = {
|
|
146
|
-
params: {
|
|
147
|
-
rule: {
|
|
148
|
-
type: {
|
|
149
|
-
kind: 'any',
|
|
150
|
-
},
|
|
151
|
-
required: false,
|
|
152
|
-
},
|
|
153
|
-
context: {
|
|
154
|
-
type: {
|
|
155
|
-
kind: 'any',
|
|
156
|
-
},
|
|
157
|
-
required: false,
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
unsafe: true,
|
|
161
|
-
source: 'rules.tjs:99',
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/*#
|
|
165
|
-
## Validate Schema
|
|
166
|
-
|
|
167
|
-
Simple schema validation for write operations.
|
|
168
|
-
Checks required fields and basic types.
|
|
169
|
-
*/
|
|
170
|
-
export function validateSchema(schema, data) {
|
|
171
|
-
const errors = []
|
|
172
|
-
|
|
173
|
-
if (!schema || typeof schema !== 'object') {
|
|
174
|
-
return { valid: true, errors: [] }
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Check required fields
|
|
178
|
-
if (schema.required && Array.isArray(schema.required)) {
|
|
179
|
-
for (const field of schema.required) {
|
|
180
|
-
if (data[field] === undefined || data[field] === null) {
|
|
181
|
-
errors.push(`Missing required field: ${field}`)
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Check field types
|
|
187
|
-
if (schema.properties && typeof schema.properties === 'object') {
|
|
188
|
-
for (const [field, spec] of Object.entries(schema.properties)) {
|
|
189
|
-
const value = data[field]
|
|
190
|
-
if (value === undefined) continue // Skip missing optional fields
|
|
191
|
-
|
|
192
|
-
const expectedType = spec.type
|
|
193
|
-
if (expectedType) {
|
|
194
|
-
const actualType = Array.isArray(value) ? 'array' : typeof value
|
|
195
|
-
if (actualType !== expectedType) {
|
|
196
|
-
errors.push(
|
|
197
|
-
`Field '${field}' expected ${expectedType}, got ${actualType}`
|
|
198
|
-
)
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
valid: errors.length === 0,
|
|
206
|
-
errors,
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
validateSchema.__tjs = {
|
|
210
|
-
params: {
|
|
211
|
-
schema: {
|
|
212
|
-
type: {
|
|
213
|
-
kind: 'any',
|
|
214
|
-
},
|
|
215
|
-
required: false,
|
|
216
|
-
},
|
|
217
|
-
data: {
|
|
218
|
-
type: {
|
|
219
|
-
kind: 'any',
|
|
220
|
-
},
|
|
221
|
-
required: false,
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
unsafe: true,
|
|
225
|
-
source: 'rules.tjs:134',
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/*#
|
|
229
|
-
## Evaluate Rule Result
|
|
230
|
-
|
|
231
|
-
Interprets the result of a rule evaluation (boolean, object, etc.)
|
|
232
|
-
into a standardized { allowed, reason } format.
|
|
233
|
-
*/
|
|
234
|
-
export function interpretRuleResult(result) {
|
|
235
|
-
if (typeof result === 'boolean') {
|
|
236
|
-
return { allowed: result, reason: result ? null : 'Access denied' }
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (typeof result === 'object' && result !== null) {
|
|
240
|
-
return {
|
|
241
|
-
allowed: !!result.allow,
|
|
242
|
-
reason: result.reason || (result.allow ? null : 'Access denied'),
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Truthy/falsy fallback
|
|
247
|
-
return { allowed: !!result, reason: result ? null : 'Access denied' }
|
|
248
|
-
}
|
|
249
|
-
interpretRuleResult.__tjs = {
|
|
250
|
-
params: {
|
|
251
|
-
result: {
|
|
252
|
-
type: {
|
|
253
|
-
kind: 'any',
|
|
254
|
-
},
|
|
255
|
-
required: false,
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
unsafe: true,
|
|
259
|
-
source: 'rules.tjs:178',
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/*#
|
|
263
|
-
## Check Role Hierarchy
|
|
264
|
-
|
|
265
|
-
Evaluates if a user has sufficient role level.
|
|
266
|
-
Role hierarchy: admin > author > user > guest
|
|
267
|
-
*/
|
|
268
|
-
const ROLE_LEVELS = {
|
|
269
|
-
admin: 100,
|
|
270
|
-
author: 50,
|
|
271
|
-
editor: 40,
|
|
272
|
-
user: 10,
|
|
273
|
-
guest: 0,
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export function hasRoleLevel(userRoles, requiredRole) {
|
|
277
|
-
const requiredLevel = ROLE_LEVELS[requiredRole] ?? 0
|
|
278
|
-
|
|
279
|
-
for (const role of userRoles || []) {
|
|
280
|
-
const level = ROLE_LEVELS[role] ?? 0
|
|
281
|
-
if (level >= requiredLevel) {
|
|
282
|
-
return true
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return false
|
|
287
|
-
}
|
|
288
|
-
hasRoleLevel.__tjs = {
|
|
289
|
-
params: {
|
|
290
|
-
userRoles: {
|
|
291
|
-
type: {
|
|
292
|
-
kind: 'any',
|
|
293
|
-
},
|
|
294
|
-
required: false,
|
|
295
|
-
},
|
|
296
|
-
requiredRole: {
|
|
297
|
-
type: {
|
|
298
|
-
kind: 'any',
|
|
299
|
-
},
|
|
300
|
-
required: false,
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
unsafe: true,
|
|
304
|
-
source: 'rules.tjs:208',
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/*#
|
|
308
|
-
## Build Rule Context
|
|
309
|
-
|
|
310
|
-
Creates the evaluation context for a security rule.
|
|
311
|
-
*/
|
|
312
|
-
export function buildRuleContext(options) {
|
|
313
|
-
const { uid, roles, method, collection, docId, doc, newData } = options
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
_uid: uid || null,
|
|
317
|
-
_roles: roles || [],
|
|
318
|
-
_isAdmin: roles?.includes('admin') || false,
|
|
319
|
-
_isAuthor: roles?.includes('author') || false,
|
|
320
|
-
_method: method,
|
|
321
|
-
_collection: collection,
|
|
322
|
-
_docId: docId,
|
|
323
|
-
doc: doc || null,
|
|
324
|
-
newData: newData || null,
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
buildRuleContext.__tjs = {
|
|
328
|
-
params: {
|
|
329
|
-
options: {
|
|
330
|
-
type: {
|
|
331
|
-
kind: 'any',
|
|
332
|
-
},
|
|
333
|
-
required: false,
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
unsafe: true,
|
|
337
|
-
source: 'rules.tjs:226',
|
|
338
|
-
}
|