Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 | 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 4x 4x 332x 332x 332x 332x 332x 332x 332x 514x 514x 514x 514x 332x 332x 332x 332x 332x 332x 1x 1x 1x 1x 1x 1x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 139x 2x 2x 2x 137x 139x 139x 2x 2x 135x 135x 139x 332x 332x 332x 332x 332x 332x 332x 332x 332x 11x 11x 11x 11x 11x 11x 11x 332x 332x 332x 332x 332x 332x 332x 2x 2x 1x 1x 1x 2x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 27x 27x 27x 18x 18x 27x 27x 332x 332x 332x 332x 332x 332x 332x 332x 332x 1x 1x 1x 1x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 313x 304x 304x 9x 8x 8x 1x 1x 1x 1x 1x 313x 332x 332x 332x 332x 332x 332x 332x 332x 332x 17x 17x 17x 17x 17x 17x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 11x 11x 11x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 51x 36x 36x 19x 6x 6x 14x 9x 51x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 14x 12x 12x 2x 2x 2x 14x 14x 332x 332x 332x 332x 332x 332x 332x 332x 332x 6x 6x 6x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 3x 1x 1x 3x 2x 3x 332x 332x 332x 332x 332x 332x 332x 332x 2x 2x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x 332x | /**
* @file ops.js
* @description The runtime primitives a compiled expression calls.
*
* ## Why these exist separately from the evaluator
*
* Avenx used to evaluate template expressions by walking an AST in the browser.
* The security boundary lived inside that walk: every property read went
* through one function with the key already resolved, so `x.constructor` and
* `x['const'+'ructor']` arrived at the same check as the same string.
*
* Expressions are now compiled to JavaScript at build time, so the walk is
* gone. The boundary is not: the compiler emits a call to {@link readMember}
* wherever the AST walk would have made one, and to {@link callFunction}
* wherever it would have invoked. The guarantees are therefore identical, and
* the cost per access drops from a recursive dispatch to one monomorphic call.
*
* This module is what a production bundle keeps. The parser and the evaluator
* that used to sit above it are build-time only.
*
* ## What is guaranteed, and what is not
*
* Guaranteed: a compiled expression cannot reach the `Function` constructor,
* cannot read or write `__proto__` / `constructor` / `prototype` however the
* key is spelled, cannot obtain a built-in prototype object, and cannot name a
* global outside {@link ALLOWED_GLOBALS}. Nothing here uses `eval` or
* `new Function`, and neither does the code the compiler emits, so a page of
* compiled Avenx expressions needs no `'unsafe-eval'`.
*
* Not guaranteed: this is not an isolation boundary against hostile expression
* source. An expression can still call any function the scope legitimately
* exposes. The boundary protects the runtime from accidents and from reaching
* outside the declared scope; it does not make it safe to compile expressions
* written by an untrusted party.
* @module lib/core/expression/ops
*/
import { AvenxError, AvenxErrorCodes } from '../runtime/AvenxError.js';
import { resolveSandboxGlobal } from '../trace/globals.js';
/**
* Globals an expression may name.
*
* Deliberately identical to the set the AST evaluator allowed, so migrating an
* application changes nothing about which globals its templates can see.
* @type {Set<string>}
*/
export const ALLOWED_GLOBALS = new Set([
'Math', 'JSON', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Date', 'Error',
'Map', 'Set', 'Promise', 'console', 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
'decodeURI', 'decodeURIComponent', 'encodeURI', 'encodeURIComponent',
'undefined', 'NaN', 'Infinity',
]);
/**
* Globals an expression may never name, whether or not the host defines them.
*
* Listed explicitly rather than inferred from `in globalThis`, because the
* diagnostic has to be the same in every environment: `localStorage` is
* restricted in a browser, in happy-dom and in bare Node, and a developer
* reading AVX_R15 should not get a different answer depending on where the
* expression happened to run.
* @type {Set<string>}
*/
export const RESTRICTED_GLOBALS = new Set([
'window', 'document', 'localStorage', 'sessionStorage', 'location', 'navigator',
'history', 'fetch', 'alert', 'confirm', 'prompt', 'setTimeout', 'setInterval',
'clearTimeout', 'clearInterval', 'XMLHttpRequest', 'WebSocket', 'global',
'globalThis', 'process', 'eval', 'Function', 'Reflect', 'Proxy', 'Symbol',
'require', 'import', 'structuredClone',
]);
/**
* Property names an expression may never read or write.
*
* Checked against the *resolved* key, so a computed access spelled
* `x['const' + 'ructor']` is rejected on the same terms as `x.constructor`.
* @type {Set<string>}
*/
export const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
/**
* The dynamic-code constructors.
*
* Reaching any of these would be arbitrary code execution, so they are refused
* as values however they were obtained.
* @type {Set<Function>}
*/
const FUNCTION_CONSTRUCTORS = new Set(
[
Function,
Object.getPrototypeOf(function* () {}).constructor,
Object.getPrototypeOf(async () => {}).constructor,
Object.getPrototypeOf(async function* () {}).constructor,
].filter((ctor) => typeof ctor === 'function'),
);
/**
* Built-in prototypes shared by every object in the realm.
*
* Handing one to an expression would let it mutate state shared with the host
* page, so they are refused as values rather than by enumerating every mutating
* method on them.
* @type {Set<object>}
*/
const PROTECTED_PROTOTYPES = new Set(
[
Object.prototype, Array.prototype, Function.prototype, String.prototype,
Number.prototype, Boolean.prototype, Date.prototype, RegExp.prototype,
Error.prototype, Map.prototype, Set.prototype, WeakMap.prototype,
WeakSet.prototype, Promise.prototype,
].filter(Boolean),
);
/**
* Raises a sandbox violation.
* @param {string} message - What was refused.
* @throws {AvenxError} Always.
*/
export function refuse(message) {
throw new AvenxError(AvenxErrorCodes.SANDBOX_VIOLATION, message);
}
/**
* Refuses a value that must never reach an expression.
* @param {any} value - The value about to be returned.
* @returns {any} The value, when it is allowed.
*/
export function guardValue(value) {
if (typeof value === 'function' && FUNCTION_CONSTRUCTORS.has(value)) {
refuse('Access to the Function constructor is blocked for security reasons.');
}
if (value !== null && (typeof value === 'object' || typeof value === 'function') && PROTECTED_PROTOTYPES.has(value)) {
refuse('Access to built-in prototypes is blocked for security reasons.');
}
return value;
}
/**
* Whether a name exists on the host global object.
* @param {string} name - The identifier.
* @returns {boolean} True when the host defines it.
*/
function isKnownGlobal(name) {
if (RESTRICTED_GLOBALS.has(name)) {
return true;
}
try {
return typeof globalThis !== 'undefined' && name in globalThis;
} catch {
return false;
}
}
/**
* Reads a property, with the key already resolved.
*
* This is the gate a source-text check cannot provide. Because the key arrives
* as a value rather than as source, there is no spelling of it that avoids the
* check.
* @param {any} object - The object to read from.
* @param {any} key - The resolved property key.
* @param {boolean} [optional] - Whether the access used `?.`.
* @returns {any} The property value.
*/
export function readMember(object, key, optional) {
if (object === null || object === undefined) {
if (optional) return undefined;
throw new TypeError(`Cannot read property "${String(key)}" of ${object === null ? 'null' : 'undefined'}`);
}
const name = typeof key === 'symbol' ? key : String(key);
if (typeof name === 'string' && FORBIDDEN_KEYS.has(name)) {
refuse(`Access to property "${name}" is blocked for security reasons.`);
}
return guardValue(object[name]);
}
/**
* Writes a property, with the key already resolved.
* @param {any} object - The object to write to.
* @param {any} key - The resolved property key.
* @param {any} value - The value to assign.
* @returns {any} The assigned value.
*/
export function writeMember(object, key, value) {
if (object === null || object === undefined) {
throw new TypeError(`Cannot set property "${String(key)}" of ${object === null ? 'null' : 'undefined'}`);
}
const name = typeof key === 'symbol' ? key : String(key);
if (typeof name === 'string' && FORBIDDEN_KEYS.has(name)) {
refuse(`Writing to property "${name}" is blocked for security reasons.`);
}
if (PROTECTED_PROTOTYPES.has(object)) {
refuse('Writing to a built-in prototype is blocked for security reasons.');
}
object[name] = value;
return value;
}
/**
* Validates a computed property key before it is used in an object literal.
* @param {any} key - The evaluated key.
* @returns {any} The key, when it is allowed.
*/
export function guardKey(key) {
const name = typeof key === 'symbol' ? key : String(key);
if (typeof name === 'string' && FORBIDDEN_KEYS.has(name)) {
refuse(`Defining property "${name}" is blocked for security reasons.`);
}
return name;
}
/**
* Calls a function on behalf of an expression.
* @param {any} fn - The callee.
* @param {any} thisArg - The receiver.
* @param {any[]} args - The arguments.
* @param {string} description - How the callee was written, for the error.
* @returns {any} The result.
*/
export function callFunction(fn, thisArg, args, description) {
if (typeof fn !== 'function') {
throw new TypeError(`${description} is not a function`);
}
if (FUNCTION_CONSTRUCTORS.has(fn)) {
refuse('Calling the Function constructor is blocked for security reasons.');
}
for (const arg of args) {
if (arg !== null && (typeof arg === 'object' || typeof arg === 'function') && PROTECTED_PROTOTYPES.has(arg)) {
refuse('Passing a built-in prototype is blocked for security reasons.');
}
}
return guardValue(fn.apply(thisArg, args));
}
/**
* Constructs a value on behalf of an expression.
* @param {any} ctor - The constructor.
* @param {any[]} args - The arguments.
* @param {string} description - How the constructor was written, for the error.
* @returns {any} The constructed value.
*/
export function construct(ctor, args, description) {
if (typeof ctor !== 'function') {
throw new TypeError(`${description} is not a constructor`);
}
if (FUNCTION_CONSTRUCTORS.has(ctor)) {
refuse('Constructing a function from a string is blocked for security reasons.');
}
return guardValue(Reflect.construct(ctor, args));
}
/**
* Resolves a free identifier against the scope, then the allowed globals.
*
* Scope first, so a component's own `Date` state key wins over the global, and
* so the reactive scope registers the dependency. A global is resolved through
* the tracer's substitution point rather than read straight off `globalThis`,
* which is what lets a recording log the non-deterministic values an expression
* observed and a replay hand the same ones back.
* @param {object} scope - The evaluation scope.
* @param {string} name - The identifier.
* @returns {any} The bound value, or undefined when nothing binds it.
*/
export function readIdentifier(scope, name) {
if (scope && name in scope) {
return guardValue(scope[name]);
}
if (ALLOWED_GLOBALS.has(name)) {
return guardValue(resolveSandboxGlobal(name));
}
if (isKnownGlobal(name)) {
refuse(
`[Avenx Sandbox Violation] Access to global object "${name}" is restricted inside templates. Decouple browser APIs into component methods.`,
);
}
return undefined;
}
/**
* Assigns to a free identifier.
* @param {object} scope - The evaluation scope.
* @param {string} name - The identifier.
* @param {any} value - The value to assign.
* @returns {any} The assigned value.
*/
export function writeIdentifier(scope, name, value) {
const bound = !!(scope && name in scope);
if (ALLOWED_GLOBALS.has(name) && !bound) {
refuse(`Assigning to the global "${name}" is blocked for security reasons.`);
}
if (!bound && isKnownGlobal(name)) {
refuse(
`[Avenx Sandbox Violation] Access to global object "${name}" is restricted inside templates. Decouple browser APIs into component methods.`,
);
}
scope[name] = value;
return value;
}
/**
* Globals that construct code from strings.
*
* Refused in every context, including `<action>` bodies, because reaching them
* is what would require `'unsafe-eval'`, and a production bundle is documented
* to run without it.
* @type {Set<string>}
*/
export const DYNAMIC_CODE_GLOBALS = new Set(['eval', 'Function']);
/**
* Returns the host global object, or an empty object when there is none.
* @returns {object} `globalThis`.
*/
function hostGlobal() {
return typeof globalThis !== 'undefined' ? globalThis : {};
}
/**
* Resolves a free identifier in an `<action>` or `<resource>` body.
*
* An action body is ordinary JavaScript (template-expressions.md), so a name
* that is not component scope resolves to the page's global, exactly as it did
* when bodies ran through `new Function` in avenx-core 0.4.x: `fetch`,
* `window`, `setInterval`, `requestAnimationFrame` and a library a page put on
* `window` are all reachable. Resolution order:
*
* 1. component scope (state, computed values, methods, bridges, `this`);
* 2. the expression allow-list (`Date`, `Math`, …), through Trace's substitution
* point, so a recording still captures the non-deterministic values an
* action observed;
* 3. the host global.
*
* Template expressions and event handlers do not use this: they keep
* {@link readIdentifier}, which refuses browser globals by documented rule.
* @param {object} scope - The evaluation scope.
* @param {string} name - The identifier.
* @returns {any} The bound value, or undefined when nothing binds it.
*/
export function readAmbientIdentifier(scope, name) {
if (scope && name in scope) {
return guardValue(scope[name]);
}
if (ALLOWED_GLOBALS.has(name)) {
return guardValue(resolveSandboxGlobal(name));
}
if (DYNAMIC_CODE_GLOBALS.has(name)) {
refuse(`"${name}" constructs code from a string and cannot be used in an action body.`);
}
return hostGlobal()[name];
}
/**
* `typeof` applied to a free identifier in an `<action>` or `<resource>` body.
* @param {object} scope - The evaluation scope.
* @param {string} name - The identifier.
* @returns {string} The type name.
*/
export function typeofAmbientIdentifier(scope, name) {
if (scope && name in scope) {
return typeof scope[name];
}
if (ALLOWED_GLOBALS.has(name)) {
return typeof resolveSandboxGlobal(name);
}
return typeof hostGlobal()[name];
}
/**
* The object an assignment to a free identifier in an action body writes to.
*
* Component scope when it binds the name; otherwise the host global when it
* already defines the name (`location = '/x'`, `onbeforeunload = …`);
* otherwise component scope, which is how an assignment to an undeclared name
* has always created a scope key. Assigning over an allow-listed global such as
* `Math` is refused, as it is in a template.
* @param {object} scope - The evaluation scope.
* @param {string} name - The identifier being assigned.
* @returns {object} The object holding the binding.
*/
export function ambientAssignmentTarget(scope, name) {
if (scope && name in scope) {
return scope;
}
if (ALLOWED_GLOBALS.has(name)) {
refuse(`Assigning to the global "${name}" is blocked for security reasons.`);
}
if (DYNAMIC_CODE_GLOBALS.has(name)) {
refuse(`"${name}" constructs code from a string and cannot be used in an action body.`);
}
const host = hostGlobal();
return name in host ? host : scope;
}
/**
* Assigns to a free identifier in an `<action>` or `<resource>` body.
* @param {object} scope - The evaluation scope.
* @param {string} name - The identifier.
* @param {any} value - The value to assign.
* @returns {any} The assigned value.
*/
export function writeAmbientIdentifier(scope, name, value) {
ambientAssignmentTarget(scope, name)[name] = value;
return value;
}
/**
* `typeof` applied to a free identifier.
*
* `typeof maybeUndefined` must not throw for an unbound name, which is the
* whole reason the operator gets used in a template.
* @param {object} scope - The evaluation scope.
* @param {string} name - The identifier.
* @returns {string} The type name.
*/
export function typeofIdentifier(scope, name) {
if (scope && name in scope) {
return typeof scope[name];
}
if (ALLOWED_GLOBALS.has(name)) {
return typeof resolveSandboxGlobal(name);
}
return 'undefined';
}
/**
* `in` applied with the right-hand side coerced, matching the interpreter.
* @param {any} key - The key to test.
* @param {any} target - The object to test against.
* @returns {boolean} Whether the key is present.
*/
export function hasIn(key, target) {
return key in Object(target);
}
/**
* The primitives a compiled expression calls, keyed by the name the generator
* emits for each.
*
* One map rather than a list repeated at every consumer. A generated module
* gets these as named imports the bundler resolves; a host that evaluates a
* bare class body instead (`avenx-core/tooling`, the Vite plugin) injects them
* from here. `lib/compiler/codegen/expression.js` declares the same names on
* the emitting side, and a test requires the two to agree.
* @type {Object<string, Function>}
*/
export const EXPRESSION_OPS = {
axRead: readMember,
axWrite: writeMember,
axCall: callFunction,
axNew: construct,
axGet: readIdentifier,
axSet: writeIdentifier,
axTypeof: typeofIdentifier,
axKey: guardKey,
axIn: hasIn,
axGetAmbient: readAmbientIdentifier,
axSetAmbient: writeAmbientIdentifier,
axTypeofAmbient: typeofAmbientIdentifier,
axAmbientTarget: ambientAssignmentTarget,
};
|