@almadar/runtime 6.47.0 → 6.49.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.
@@ -1,6 +1,7 @@
1
- import { createTickScheduler, EventBus, createUnifiedLoader, MockPersistenceAdapter, InMemoryPersistence, preprocessSchema, normalizeCallSiteConfigToValues, StateMachineManager, parseDurationString, createContextFromBindings, validateEventPayload, formatPayloadValidationError, collectDeclaredConfigDefaults, resolveCallSitePayloadCaptures, collectDeclaredEntityDefaults, EffectExecutor } from './chunk-FIBEYLME.js';
2
- export { InMemoryPersistence, collectDeclaredConfigDefaults } from './chunk-FIBEYLME.js';
1
+ import { createTickScheduler, EventBus, createUnifiedLoader, MockPersistenceAdapter, InMemoryPersistence, preprocessSchema, normalizeCallSiteConfigToValues, StateMachineManager, parseDurationString, validateEventPayload, formatPayloadValidationError, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, EffectExecutor } from './chunk-ML75GCRO.js';
2
+ export { InMemoryPersistence, collectDeclaredConfigDefaults } from './chunk-ML75GCRO.js';
3
3
  import { isValidCronExpression } from './chunk-OU3ITB5S.js';
4
+ import { createContextFromBindings, resolveCallSitePayloadCaptures, applyRowAccess, checkMutationAccess, accessDeniedMessage } from './chunk-XLMDWRMB.js';
4
5
  import './chunk-T4VDAB4C.js';
5
6
  import './chunk-SCRAHWOC.js';
6
7
  import './chunk-MLKGABMK.js';
@@ -33,41 +34,6 @@ function buildSourceMatcher(src, listenerOrbital) {
33
34
  const wantedTrait = src.trait;
34
35
  return (source) => !!source && source.orbital === wantedOrbital && source.trait === wantedTrait;
35
36
  }
36
- function applyRowAccess(rows, policy, filter, bindings) {
37
- if (policy === void 0 && filter === void 0) {
38
- return rows;
39
- }
40
- const predicates = [policy, filter].filter((p) => p !== void 0);
41
- return rows.filter((entity) => {
42
- const ctx = createContextFromBindings(
43
- { entity, payload: bindings.payload, current: entity, user: bindings.user, config: bindings.config },
44
- false
45
- );
46
- return predicates.every((predicate) => {
47
- try {
48
- return Boolean(evaluate(predicate, ctx));
49
- } catch {
50
- return false;
51
- }
52
- });
53
- });
54
- }
55
- function checkMutationAccess(row, policy, bindings) {
56
- if (policy === void 0) {
57
- return true;
58
- }
59
- const ctx = createContextFromBindings(
60
- { entity: row, payload: bindings.payload, current: row, user: bindings.user, config: bindings.config },
61
- false
62
- );
63
- try {
64
- return Boolean(evaluate(policy, ctx));
65
- } catch {
66
- return false;
67
- }
68
- }
69
-
70
- // src/OrbitalServerRuntime.ts
71
37
  var _resolvedNodeRequire = null;
72
38
  function nodeRequire(modulePath) {
73
39
  if (!_resolvedNodeRequire) {
@@ -1356,9 +1322,7 @@ var OrbitalServerRuntime = class {
1356
1322
  switch (action) {
1357
1323
  case "create": {
1358
1324
  if (!checkMutationAccess(data || {}, mutationPolicy, accessBindings)) {
1359
- throw new Error(
1360
- `@create denied: the declared access policy for '${type}' rejected this row`
1361
- );
1325
+ throw new Error(accessDeniedMessage("create", type));
1362
1326
  }
1363
1327
  const { id } = await this.persistence.create(type, data || {});
1364
1328
  resultData = { id, ...data || {} };
@@ -1370,9 +1334,7 @@ var OrbitalServerRuntime = class {
1370
1334
  if (mutationPolicy !== void 0) {
1371
1335
  const existing = await this.persistence.getById(type, updateId);
1372
1336
  if (!existing || !checkMutationAccess(existing, mutationPolicy, accessBindings)) {
1373
- throw new Error(
1374
- `@update denied: the declared access policy for '${type}' rejected this row`
1375
- );
1337
+ throw new Error(accessDeniedMessage("update", type));
1376
1338
  }
1377
1339
  }
1378
1340
  await this.persistence.update(type, updateId, data || {});
@@ -1388,9 +1350,7 @@ var OrbitalServerRuntime = class {
1388
1350
  if (mutationPolicy !== void 0) {
1389
1351
  const existing = await this.persistence.getById(type, deleteId);
1390
1352
  if (!existing || !checkMutationAccess(existing, mutationPolicy, accessBindings)) {
1391
- throw new Error(
1392
- `@delete denied: the declared access policy for '${type}' rejected this row`
1393
- );
1353
+ throw new Error(accessDeniedMessage("delete", type));
1394
1354
  }
1395
1355
  }
1396
1356
  await this.enforceOnDeleteRules(type, deleteId);
@@ -1,11 +1,10 @@
1
1
  import { parseCron, cronMinuteKey, cronMatches } from './chunk-OU3ITB5S.js';
2
+ import { createContextFromBindings, interpolateValue, deferEntityBindings } from './chunk-XLMDWRMB.js';
2
3
  import { seedRandom, randomArrayElement, randomInt, shuffleArray, randomPastDate } from './chunk-T4VDAB4C.js';
3
4
  import { collectTraitRefsFromValue, collectTraitRefsFromEffects } from './chunk-SCRAHWOC.js';
4
5
  import { createLogger, setNamespaceLevel } from '@almadar/logger';
5
- import { createMinimalContext, resolveBinding, evaluate, evaluateGuard, SExpressionEvaluator } from '@almadar/evaluator';
6
- export { createMinimalContext } from '@almadar/evaluator';
7
- import { isKnownStdOperator } from '@almadar/std/registry';
8
- import { containsEntityBinding, containsPayloadBinding, RENDER_BINDING_MARKER, OrbitalSchemaSchema, isInlineTrait, isEntityCall, isEntityReference, parseEntityRef, parseImportedTraitRef, isPageReference, isPageReferenceString, isPageReferenceObject, parsePageRef, isReferenceConfigType, configRefEventKnob, normalizeCallSiteConfigToValues, resolveConfigRefEventName } from '@almadar/core';
6
+ import { evaluateGuard, SExpressionEvaluator } from '@almadar/evaluator';
7
+ import { OrbitalSchemaSchema, isInlineTrait, isEntityCall, isEntityReference, parseEntityRef, parseImportedTraitRef, isPageReference, isPageReferenceString, isPageReferenceObject, parsePageRef, isReferenceConfigType, configRefEventKnob, normalizeCallSiteConfigToValues, resolveConfigRefEventName } from '@almadar/core';
9
8
  export { normalizeCallSiteConfigToValues } from '@almadar/core';
10
9
  import { sampleRowCount, sampleRow } from '@almadar/core/mock';
11
10
 
@@ -289,292 +288,6 @@ function parseDurationString(interval) {
289
288
  function isValidDurationString(interval) {
290
289
  return /^(\d+)(ms|s|m|h)?$/.test(interval);
291
290
  }
292
- var bindLog = createLogger("almadar:runtime:bindings");
293
- setNamespaceLevel("almadar:runtime:bindings", "WARN");
294
- var deferLog = createLogger("almadar:runtime:defer");
295
- var renderLog = createLogger("almadar:runtime:render-ui");
296
- var CLIENT_ONLY_BINDING_ROOTS = /* @__PURE__ */ new Set(["trait"]);
297
- var CALLSITE_PAYLOAD_PREFIX = "@callsitePayload.";
298
- function payloadValueToConfigValue(v) {
299
- if (v === null || v === void 0) return null;
300
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
301
- if (v instanceof Date) return v.toISOString();
302
- if (Array.isArray(v)) return v.map(payloadValueToConfigValue);
303
- if (typeof v === "object") {
304
- const obj = {};
305
- for (const [k, val] of Object.entries(v)) obj[k] = payloadValueToConfigValue(val);
306
- return obj;
307
- }
308
- return String(v);
309
- }
310
- function resolveCallSitePayloadCaptures(config, payload) {
311
- let ctx;
312
- const out = {};
313
- for (const [key, value] of Object.entries(config)) {
314
- if (typeof value === "string" && value.startsWith(CALLSITE_PAYLOAD_PREFIX)) {
315
- const field = value.slice(CALLSITE_PAYLOAD_PREFIX.length);
316
- if (!ctx) ctx = createMinimalContext({}, payload ?? {}, "idle");
317
- out[key] = payloadValueToConfigValue(resolveBinding(`@payload.${field}`, ctx));
318
- } else {
319
- out[key] = value;
320
- }
321
- }
322
- return out;
323
- }
324
- function isClientOnlyBinding(value) {
325
- if (!value.startsWith("@")) return false;
326
- const afterAt = value.slice(1);
327
- const firstDot = afterAt.indexOf(".");
328
- const root = firstDot === -1 ? afterAt : afterAt.slice(0, firstDot);
329
- return CLIENT_ONLY_BINDING_ROOTS.has(root);
330
- }
331
- function interpolateProps(props, ctx) {
332
- const result = {};
333
- let anyChanged = false;
334
- for (const [key, value] of Object.entries(props)) {
335
- const interpolated = interpolateValue(value, ctx);
336
- result[key] = interpolated;
337
- if (interpolated !== value) anyChanged = true;
338
- }
339
- const entityBindingRaw = props["entity"];
340
- const typeBindingRaw = props["type"];
341
- const patternType = typeof typeBindingRaw === "string" ? typeBindingRaw : void 0;
342
- if (typeof entityBindingRaw === "string") {
343
- renderLog.debug("interpolateProps:entity", () => {
344
- const resolvedEntity = result["entity"];
345
- const resolvedRow = resolvedEntity !== null && typeof resolvedEntity === "object" && !Array.isArray(resolvedEntity) ? resolvedEntity : null;
346
- const ctxRow = ctx.payload["row"];
347
- const ctxPayloadKeys = Object.keys(ctx.payload).join(",");
348
- const payloadDataRaw = ctx.payload["data"];
349
- const payloadDataLen = Array.isArray(payloadDataRaw) ? payloadDataRaw.length : null;
350
- const ctxEntityRaw = ctx.entity;
351
- const ctxEntityLen = Array.isArray(ctxEntityRaw) ? ctxEntityRaw.length : null;
352
- const resolvedLen = Array.isArray(resolvedEntity) ? resolvedEntity.length : null;
353
- return {
354
- patternType,
355
- entityBinding: entityBindingRaw,
356
- resolvedIsObject: resolvedRow !== null,
357
- resolvedIsArray: Array.isArray(resolvedEntity),
358
- resolvedLen,
359
- resolvedEqualsCtxRow: ctxRow !== void 0 && resolvedRow !== null && resolvedRow === ctxRow,
360
- resolvedRowId: resolvedRow?.id,
361
- ctxPayloadKeys,
362
- ctxPayloadDataLen: payloadDataLen,
363
- ctxEntityIsArray: Array.isArray(ctxEntityRaw),
364
- ctxEntityLen
365
- };
366
- });
367
- }
368
- if (patternType === "form-section" || patternType === "form") {
369
- bindLog.debug("form-binding", () => {
370
- const modeRaw = result["mode"];
371
- const submitRaw = result["submitEvent"];
372
- const cancelRaw = result["cancelEvent"];
373
- return {
374
- patternType,
375
- mode: typeof modeRaw === "string" ? modeRaw : void 0,
376
- submitEvent: typeof submitRaw === "string" ? submitRaw : void 0,
377
- cancelEvent: typeof cancelRaw === "string" ? cancelRaw : void 0,
378
- entity: JSON.stringify(result["entity"] ?? null),
379
- fields: JSON.stringify(result["fields"] ?? null)
380
- };
381
- });
382
- }
383
- return anyChanged ? result : props;
384
- }
385
- function interpolateValue(value, ctx) {
386
- if (value === null || value === void 0) {
387
- return value;
388
- }
389
- if (typeof value === "string") {
390
- return interpolateString(value, ctx);
391
- }
392
- if (Array.isArray(value)) {
393
- return interpolateArray(value, ctx);
394
- }
395
- if (typeof value === "object") {
396
- return interpolateProps(value, ctx);
397
- }
398
- return value;
399
- }
400
- function deferEntityBindings(value, ctx, configHops = 0) {
401
- if (typeof value === "string") {
402
- if (containsEntityBinding(value) && !containsPayloadBinding(value)) {
403
- return { [RENDER_BINDING_MARKER]: true, expression: value };
404
- }
405
- if (value.startsWith("@config.") && !value.includes(" ") && configHops < 8) {
406
- const hop = ctx.config?.[value.slice("@config.".length)];
407
- deferLog.debug("defer:config-hop", () => ({
408
- forward: value,
409
- hopType: typeof hop,
410
- hopPreview: typeof hop === "string" ? hop : Array.isArray(hop) ? "array" : hop === void 0 ? "undefined" : "object"
411
- }));
412
- if (hop !== void 0 && hop !== value) {
413
- return deferEntityBindings(hop, ctx, configHops + 1);
414
- }
415
- }
416
- return interpolateValue(value, ctx);
417
- }
418
- if (Array.isArray(value)) {
419
- if (value.length === 3 && value[0] === "fn" && typeof value[1] === "string") {
420
- return value;
421
- }
422
- if (isSExpression(value)) {
423
- if (containsEntityBinding(value) && !containsPayloadBinding(value)) {
424
- return { [RENDER_BINDING_MARKER]: true, expression: value };
425
- }
426
- return interpolateValue(value, ctx);
427
- }
428
- return value.map((item) => deferEntityBindings(item, ctx));
429
- }
430
- if (value !== null && typeof value === "object") {
431
- const out = {};
432
- for (const [key, item] of Object.entries(value)) {
433
- out[key] = deferEntityBindings(item, ctx);
434
- }
435
- return out;
436
- }
437
- return value;
438
- }
439
- var inFlightConfigRecursions = /* @__PURE__ */ new Set();
440
- function interpolateString(value, ctx) {
441
- if (value.startsWith("@") && isPureBinding(value)) {
442
- if (isClientOnlyBinding(value)) {
443
- bindLog.debug("passthrough:client-only", { binding: value });
444
- return value;
445
- }
446
- const resolved = resolveBinding(value, ctx);
447
- bindLog.debug("resolve", { binding: value, resolvedType: typeof resolved });
448
- if (value.startsWith("@config.") && resolved !== null && typeof resolved === "object" && containsBindings(resolved) && !inFlightConfigRecursions.has(value)) {
449
- inFlightConfigRecursions.add(value);
450
- try {
451
- bindLog.debug("resolve:config-recurse", { binding: value });
452
- return interpolateValue(resolved, ctx);
453
- } finally {
454
- inFlightConfigRecursions.delete(value);
455
- }
456
- }
457
- return resolved;
458
- }
459
- if (value.includes("@")) {
460
- return interpolateEmbeddedBindings(value, ctx);
461
- }
462
- return value;
463
- }
464
- function isPureBinding(value) {
465
- return /^@[\w]+(?:\[\d+\])*(?:\.[\w]+(?:\[\d+\])*)*$/.test(value);
466
- }
467
- function interpolateEmbeddedBindings(value, ctx) {
468
- return value.replace(/@[\w]+(?:\[\d+\])*(?:\.[\w]+(?:\[\d+\])*)*/g, (match) => {
469
- if (isClientOnlyBinding(match)) {
470
- return match;
471
- }
472
- const resolved = resolveBinding(match, ctx);
473
- return resolved !== void 0 ? String(resolved) : match;
474
- });
475
- }
476
- function interpolateArray(value, ctx) {
477
- if (value.length === 0) {
478
- return value;
479
- }
480
- if (Array.isArray(value) && value.length === 3 && value[0] === "fn" && typeof value[1] === "string") {
481
- return value;
482
- }
483
- if (isSExpression(value)) {
484
- const result = evaluate(value, ctx);
485
- bindLog.debug("sexpr:eval", () => ({
486
- operator: typeof value[0] === "string" ? value[0] : "<non-string>",
487
- argCount: value.length - 1,
488
- inputJson: JSON.stringify(value).slice(0, 300),
489
- resultType: typeof result,
490
- resultJson: typeof result === "object" && result !== null ? JSON.stringify(result).slice(0, 2e3) : String(result)
491
- }));
492
- return result;
493
- }
494
- const mapped = [];
495
- let anyChanged = false;
496
- for (let i = 0; i < value.length; i++) {
497
- const item = value[i];
498
- if (Array.isArray(item) && isRenderChildrenMap(item)) {
499
- const expanded = evaluate(item, ctx);
500
- if (Array.isArray(expanded)) {
501
- for (const node of expanded) mapped.push(node);
502
- }
503
- anyChanged = true;
504
- continue;
505
- }
506
- const interpolated = interpolateValue(item, ctx);
507
- mapped.push(interpolated);
508
- if (interpolated !== item) anyChanged = true;
509
- }
510
- return anyChanged ? mapped : value;
511
- }
512
- function isRenderChildrenMap(value) {
513
- if (value.length !== 3 || value[0] !== "array/map") return false;
514
- const lambda = value[2];
515
- return Array.isArray(lambda) && lambda.length === 3 && lambda[0] === "fn" && typeof lambda[1] === "string";
516
- }
517
- function isSExpression(value) {
518
- if (value.length === 0) return false;
519
- const first = value[0];
520
- if (typeof first !== "string") return false;
521
- if (isKnownStdOperator(first)) return true;
522
- if (first.includes("/")) return true;
523
- if (first === "lambda" || first === "let") return true;
524
- return false;
525
- }
526
- function containsBindings(value) {
527
- if (typeof value === "string") {
528
- return value.includes("@");
529
- }
530
- if (Array.isArray(value)) {
531
- return value.some(containsBindings);
532
- }
533
- if (value !== null && typeof value === "object") {
534
- return Object.values(value).some(containsBindings);
535
- }
536
- return false;
537
- }
538
- function extractBindings(value) {
539
- const bindings = [];
540
- function collect(v) {
541
- if (typeof v === "string") {
542
- const matches = v.match(/@[\w]+(?:\.[\w]+)*/g);
543
- if (matches) {
544
- bindings.push(...matches);
545
- }
546
- } else if (Array.isArray(v)) {
547
- v.forEach(collect);
548
- } else if (v !== null && typeof v === "object") {
549
- Object.values(v).forEach(collect);
550
- }
551
- }
552
- collect(value);
553
- return [...new Set(bindings)];
554
- }
555
- function createContextFromBindings(bindings, strictBindings, contextExtensions) {
556
- const ctx = createMinimalContext(
557
- bindings.entity || {},
558
- bindings.payload || {},
559
- bindings.state || "idle"
560
- );
561
- if (strictBindings) {
562
- ctx.strictBindings = true;
563
- }
564
- if (bindings.config) {
565
- ctx.config = bindings.config;
566
- }
567
- if (bindings.user) {
568
- ctx.user = bindings.user;
569
- }
570
- if (bindings.locals) {
571
- ctx.locals = bindings.locals;
572
- }
573
- if (contextExtensions) {
574
- Object.assign(ctx, contextExtensions);
575
- }
576
- return ctx;
577
- }
578
291
  var smLog = createLogger("almadar:runtime:sm");
579
292
  function findInitialState(trait) {
580
293
  if (!trait.states || trait.states.length === 0) {
@@ -4943,4 +4656,4 @@ var InMemoryPersistence = class {
4943
4656
  }
4944
4657
  };
4945
4658
 
4946
- export { CALLSITE_PAYLOAD_PREFIX, EffectExecutor, EventBus, HANDLER_MANIFEST, InMemoryPersistence, MockPersistenceAdapter, StateMachineManager, TickScheduler, buildEmitsFromTraits, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, containsBindings, createContextFromBindings, createInitialTraitState, createMockPersistence, createTestExecutor, createTickScheduler, createUnifiedLoader, deferEntityBindings, extractBindings, findInitialState, findTransition, formatPayloadValidationError, getIsolatedCollectionName, getNamespacedEvent, interpolateProps, interpolateValue, isBrowser, isElectron, isNamespacedEvent, isNode, isValidDurationString, normalizeEventKey, parseDurationString, parseNamespacedEvent, preprocessSchema, processEvent, resolveCallSitePayloadCaptures, validateEventPayload, validatePayloadShapes };
4659
+ export { EffectExecutor, EventBus, HANDLER_MANIFEST, InMemoryPersistence, MockPersistenceAdapter, StateMachineManager, TickScheduler, buildEmitsFromTraits, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, createInitialTraitState, createMockPersistence, createTestExecutor, createTickScheduler, createUnifiedLoader, findInitialState, findTransition, formatPayloadValidationError, getIsolatedCollectionName, getNamespacedEvent, isBrowser, isElectron, isNamespacedEvent, isNode, isValidDurationString, normalizeEventKey, parseDurationString, parseNamespacedEvent, preprocessSchema, processEvent, validateEventPayload, validatePayloadShapes };
@@ -0,0 +1,335 @@
1
+ import { createMinimalContext, resolveBinding, evaluate } from '@almadar/evaluator';
2
+ export { createMinimalContext } from '@almadar/evaluator';
3
+ import { isKnownStdOperator } from '@almadar/std/registry';
4
+ import { containsEntityBinding, containsPayloadBinding, RENDER_BINDING_MARKER } from '@almadar/core';
5
+ import { createLogger, setNamespaceLevel } from '@almadar/logger';
6
+
7
+ // src/entityAccess.ts
8
+ var bindLog = createLogger("almadar:runtime:bindings");
9
+ setNamespaceLevel("almadar:runtime:bindings", "WARN");
10
+ var deferLog = createLogger("almadar:runtime:defer");
11
+ var renderLog = createLogger("almadar:runtime:render-ui");
12
+ var CLIENT_ONLY_BINDING_ROOTS = /* @__PURE__ */ new Set(["trait"]);
13
+ var CALLSITE_PAYLOAD_PREFIX = "@callsitePayload.";
14
+ function payloadValueToConfigValue(v) {
15
+ if (v === null || v === void 0) return null;
16
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
17
+ if (v instanceof Date) return v.toISOString();
18
+ if (Array.isArray(v)) return v.map(payloadValueToConfigValue);
19
+ if (typeof v === "object") {
20
+ const obj = {};
21
+ for (const [k, val] of Object.entries(v)) obj[k] = payloadValueToConfigValue(val);
22
+ return obj;
23
+ }
24
+ return String(v);
25
+ }
26
+ function resolveCallSitePayloadCaptures(config, payload) {
27
+ let ctx;
28
+ const out = {};
29
+ for (const [key, value] of Object.entries(config)) {
30
+ if (typeof value === "string" && value.startsWith(CALLSITE_PAYLOAD_PREFIX)) {
31
+ const field = value.slice(CALLSITE_PAYLOAD_PREFIX.length);
32
+ if (!ctx) ctx = createMinimalContext({}, payload ?? {}, "idle");
33
+ out[key] = payloadValueToConfigValue(resolveBinding(`@payload.${field}`, ctx));
34
+ } else {
35
+ out[key] = value;
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+ function isClientOnlyBinding(value) {
41
+ if (!value.startsWith("@")) return false;
42
+ const afterAt = value.slice(1);
43
+ const firstDot = afterAt.indexOf(".");
44
+ const root = firstDot === -1 ? afterAt : afterAt.slice(0, firstDot);
45
+ return CLIENT_ONLY_BINDING_ROOTS.has(root);
46
+ }
47
+ function interpolateProps(props, ctx) {
48
+ const result = {};
49
+ let anyChanged = false;
50
+ for (const [key, value] of Object.entries(props)) {
51
+ const interpolated = interpolateValue(value, ctx);
52
+ result[key] = interpolated;
53
+ if (interpolated !== value) anyChanged = true;
54
+ }
55
+ const entityBindingRaw = props["entity"];
56
+ const typeBindingRaw = props["type"];
57
+ const patternType = typeof typeBindingRaw === "string" ? typeBindingRaw : void 0;
58
+ if (typeof entityBindingRaw === "string") {
59
+ renderLog.debug("interpolateProps:entity", () => {
60
+ const resolvedEntity = result["entity"];
61
+ const resolvedRow = resolvedEntity !== null && typeof resolvedEntity === "object" && !Array.isArray(resolvedEntity) ? resolvedEntity : null;
62
+ const ctxRow = ctx.payload["row"];
63
+ const ctxPayloadKeys = Object.keys(ctx.payload).join(",");
64
+ const payloadDataRaw = ctx.payload["data"];
65
+ const payloadDataLen = Array.isArray(payloadDataRaw) ? payloadDataRaw.length : null;
66
+ const ctxEntityRaw = ctx.entity;
67
+ const ctxEntityLen = Array.isArray(ctxEntityRaw) ? ctxEntityRaw.length : null;
68
+ const resolvedLen = Array.isArray(resolvedEntity) ? resolvedEntity.length : null;
69
+ return {
70
+ patternType,
71
+ entityBinding: entityBindingRaw,
72
+ resolvedIsObject: resolvedRow !== null,
73
+ resolvedIsArray: Array.isArray(resolvedEntity),
74
+ resolvedLen,
75
+ resolvedEqualsCtxRow: ctxRow !== void 0 && resolvedRow !== null && resolvedRow === ctxRow,
76
+ resolvedRowId: resolvedRow?.id,
77
+ ctxPayloadKeys,
78
+ ctxPayloadDataLen: payloadDataLen,
79
+ ctxEntityIsArray: Array.isArray(ctxEntityRaw),
80
+ ctxEntityLen
81
+ };
82
+ });
83
+ }
84
+ if (patternType === "form-section" || patternType === "form") {
85
+ bindLog.debug("form-binding", () => {
86
+ const modeRaw = result["mode"];
87
+ const submitRaw = result["submitEvent"];
88
+ const cancelRaw = result["cancelEvent"];
89
+ return {
90
+ patternType,
91
+ mode: typeof modeRaw === "string" ? modeRaw : void 0,
92
+ submitEvent: typeof submitRaw === "string" ? submitRaw : void 0,
93
+ cancelEvent: typeof cancelRaw === "string" ? cancelRaw : void 0,
94
+ entity: JSON.stringify(result["entity"] ?? null),
95
+ fields: JSON.stringify(result["fields"] ?? null)
96
+ };
97
+ });
98
+ }
99
+ return anyChanged ? result : props;
100
+ }
101
+ function interpolateValue(value, ctx) {
102
+ if (value === null || value === void 0) {
103
+ return value;
104
+ }
105
+ if (typeof value === "string") {
106
+ return interpolateString(value, ctx);
107
+ }
108
+ if (Array.isArray(value)) {
109
+ return interpolateArray(value, ctx);
110
+ }
111
+ if (typeof value === "object") {
112
+ return interpolateProps(value, ctx);
113
+ }
114
+ return value;
115
+ }
116
+ function deferEntityBindings(value, ctx, configHops = 0) {
117
+ if (typeof value === "string") {
118
+ if (containsEntityBinding(value) && !containsPayloadBinding(value)) {
119
+ return { [RENDER_BINDING_MARKER]: true, expression: value };
120
+ }
121
+ if (value.startsWith("@config.") && !value.includes(" ") && configHops < 8) {
122
+ const hop = ctx.config?.[value.slice("@config.".length)];
123
+ deferLog.debug("defer:config-hop", () => ({
124
+ forward: value,
125
+ hopType: typeof hop,
126
+ hopPreview: typeof hop === "string" ? hop : Array.isArray(hop) ? "array" : hop === void 0 ? "undefined" : "object"
127
+ }));
128
+ if (hop !== void 0 && hop !== value) {
129
+ return deferEntityBindings(hop, ctx, configHops + 1);
130
+ }
131
+ }
132
+ return interpolateValue(value, ctx);
133
+ }
134
+ if (Array.isArray(value)) {
135
+ if (value.length === 3 && value[0] === "fn" && typeof value[1] === "string") {
136
+ return value;
137
+ }
138
+ if (isSExpression(value)) {
139
+ if (containsEntityBinding(value) && !containsPayloadBinding(value)) {
140
+ return { [RENDER_BINDING_MARKER]: true, expression: value };
141
+ }
142
+ return interpolateValue(value, ctx);
143
+ }
144
+ return value.map((item) => deferEntityBindings(item, ctx));
145
+ }
146
+ if (value !== null && typeof value === "object") {
147
+ const out = {};
148
+ for (const [key, item] of Object.entries(value)) {
149
+ out[key] = deferEntityBindings(item, ctx);
150
+ }
151
+ return out;
152
+ }
153
+ return value;
154
+ }
155
+ var inFlightConfigRecursions = /* @__PURE__ */ new Set();
156
+ function interpolateString(value, ctx) {
157
+ if (value.startsWith("@") && isPureBinding(value)) {
158
+ if (isClientOnlyBinding(value)) {
159
+ bindLog.debug("passthrough:client-only", { binding: value });
160
+ return value;
161
+ }
162
+ const resolved = resolveBinding(value, ctx);
163
+ bindLog.debug("resolve", { binding: value, resolvedType: typeof resolved });
164
+ if (value.startsWith("@config.") && resolved !== null && typeof resolved === "object" && containsBindings(resolved) && !inFlightConfigRecursions.has(value)) {
165
+ inFlightConfigRecursions.add(value);
166
+ try {
167
+ bindLog.debug("resolve:config-recurse", { binding: value });
168
+ return interpolateValue(resolved, ctx);
169
+ } finally {
170
+ inFlightConfigRecursions.delete(value);
171
+ }
172
+ }
173
+ return resolved;
174
+ }
175
+ if (value.includes("@")) {
176
+ return interpolateEmbeddedBindings(value, ctx);
177
+ }
178
+ return value;
179
+ }
180
+ function isPureBinding(value) {
181
+ return /^@[\w]+(?:\[\d+\])*(?:\.[\w]+(?:\[\d+\])*)*$/.test(value);
182
+ }
183
+ function interpolateEmbeddedBindings(value, ctx) {
184
+ return value.replace(/@[\w]+(?:\[\d+\])*(?:\.[\w]+(?:\[\d+\])*)*/g, (match) => {
185
+ if (isClientOnlyBinding(match)) {
186
+ return match;
187
+ }
188
+ const resolved = resolveBinding(match, ctx);
189
+ return resolved !== void 0 ? String(resolved) : match;
190
+ });
191
+ }
192
+ function interpolateArray(value, ctx) {
193
+ if (value.length === 0) {
194
+ return value;
195
+ }
196
+ if (Array.isArray(value) && value.length === 3 && value[0] === "fn" && typeof value[1] === "string") {
197
+ return value;
198
+ }
199
+ if (isSExpression(value)) {
200
+ const result = evaluate(value, ctx);
201
+ bindLog.debug("sexpr:eval", () => ({
202
+ operator: typeof value[0] === "string" ? value[0] : "<non-string>",
203
+ argCount: value.length - 1,
204
+ inputJson: JSON.stringify(value).slice(0, 300),
205
+ resultType: typeof result,
206
+ resultJson: typeof result === "object" && result !== null ? JSON.stringify(result).slice(0, 2e3) : String(result)
207
+ }));
208
+ return result;
209
+ }
210
+ const mapped = [];
211
+ let anyChanged = false;
212
+ for (let i = 0; i < value.length; i++) {
213
+ const item = value[i];
214
+ if (Array.isArray(item) && isRenderChildrenMap(item)) {
215
+ const expanded = evaluate(item, ctx);
216
+ if (Array.isArray(expanded)) {
217
+ for (const node of expanded) mapped.push(node);
218
+ }
219
+ anyChanged = true;
220
+ continue;
221
+ }
222
+ const interpolated = interpolateValue(item, ctx);
223
+ mapped.push(interpolated);
224
+ if (interpolated !== item) anyChanged = true;
225
+ }
226
+ return anyChanged ? mapped : value;
227
+ }
228
+ function isRenderChildrenMap(value) {
229
+ if (value.length !== 3 || value[0] !== "array/map") return false;
230
+ const lambda = value[2];
231
+ return Array.isArray(lambda) && lambda.length === 3 && lambda[0] === "fn" && typeof lambda[1] === "string";
232
+ }
233
+ function isSExpression(value) {
234
+ if (value.length === 0) return false;
235
+ const first = value[0];
236
+ if (typeof first !== "string") return false;
237
+ if (isKnownStdOperator(first)) return true;
238
+ if (first.includes("/")) return true;
239
+ if (first === "lambda" || first === "let") return true;
240
+ return false;
241
+ }
242
+ function containsBindings(value) {
243
+ if (typeof value === "string") {
244
+ return value.includes("@");
245
+ }
246
+ if (Array.isArray(value)) {
247
+ return value.some(containsBindings);
248
+ }
249
+ if (value !== null && typeof value === "object") {
250
+ return Object.values(value).some(containsBindings);
251
+ }
252
+ return false;
253
+ }
254
+ function extractBindings(value) {
255
+ const bindings = [];
256
+ function collect(v) {
257
+ if (typeof v === "string") {
258
+ const matches = v.match(/@[\w]+(?:\.[\w]+)*/g);
259
+ if (matches) {
260
+ bindings.push(...matches);
261
+ }
262
+ } else if (Array.isArray(v)) {
263
+ v.forEach(collect);
264
+ } else if (v !== null && typeof v === "object") {
265
+ Object.values(v).forEach(collect);
266
+ }
267
+ }
268
+ collect(value);
269
+ return [...new Set(bindings)];
270
+ }
271
+ function createContextFromBindings(bindings, strictBindings, contextExtensions) {
272
+ const ctx = createMinimalContext(
273
+ bindings.entity || {},
274
+ bindings.payload || {},
275
+ bindings.state || "idle"
276
+ );
277
+ if (strictBindings) {
278
+ ctx.strictBindings = true;
279
+ }
280
+ if (bindings.config) {
281
+ ctx.config = bindings.config;
282
+ }
283
+ if (bindings.user) {
284
+ ctx.user = bindings.user;
285
+ }
286
+ if (bindings.locals) {
287
+ ctx.locals = bindings.locals;
288
+ }
289
+ if (contextExtensions) {
290
+ Object.assign(ctx, contextExtensions);
291
+ }
292
+ return ctx;
293
+ }
294
+
295
+ // src/entityAccess.ts
296
+ function accessDeniedMessage(op, entityType) {
297
+ return `@${op} denied: the declared access policy for '${entityType}' rejected this row`;
298
+ }
299
+ function applyRowAccess(rows, policy, filter, bindings) {
300
+ if (policy === void 0 && filter === void 0) {
301
+ return rows;
302
+ }
303
+ const predicates = [policy, filter].filter((p) => p !== void 0);
304
+ return rows.filter((entity) => {
305
+ const ctx = createContextFromBindings(
306
+ { entity, payload: bindings.payload, user: bindings.user, config: bindings.config },
307
+ false
308
+ );
309
+ return predicates.every((predicate) => {
310
+ try {
311
+ return Boolean(evaluate(predicate, ctx));
312
+ } catch (err) {
313
+ bindings.onPredicateError?.(err);
314
+ return false;
315
+ }
316
+ });
317
+ });
318
+ }
319
+ function checkMutationAccess(row, policy, bindings) {
320
+ if (policy === void 0) {
321
+ return true;
322
+ }
323
+ const ctx = createContextFromBindings(
324
+ { entity: row, payload: bindings.payload, user: bindings.user, config: bindings.config },
325
+ false
326
+ );
327
+ try {
328
+ return Boolean(evaluate(policy, ctx));
329
+ } catch (err) {
330
+ bindings.onPredicateError?.(err);
331
+ return false;
332
+ }
333
+ }
334
+
335
+ export { CALLSITE_PAYLOAD_PREFIX, accessDeniedMessage, applyRowAccess, checkMutationAccess, containsBindings, createContextFromBindings, deferEntityBindings, extractBindings, interpolateProps, interpolateValue, resolveCallSitePayloadCaptures };
@@ -0,0 +1,77 @@
1
+ import { UserContext, EventPayload, TraitConfigObject, EntityRow, SExpr } from '@almadar/core';
2
+
3
+ /**
4
+ * Row-level entity access — the single owner of every declared `@read`/
5
+ * `@create`/`@update`/`@delete` directive AND the call-site fetch `filter:`.
6
+ *
7
+ * The JS twin of `orbital-core/src/runtime/entity_access.rs`. An access
8
+ * directive is a per-row predicate evaluated with `@entity` bound to the
9
+ * candidate row (or, for `@create`, the incoming data — no row exists yet)
10
+ * and `@user` bound to the viewer. Every fetch/persist path applies it
11
+ * through here, so the ownership-scoped result narrows identically no
12
+ * matter which trait issued it — the un-bypassable twin of a call-site
13
+ * `filter:`, which a sibling trait can skip simply by not declaring one
14
+ * (R-FETCH-SCOPE-SIBLING-BYPASS).
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+
19
+ /** Bind shape every access check evaluates against. */
20
+ interface AccessBindings {
21
+ user?: UserContext;
22
+ payload?: EventPayload;
23
+ /**
24
+ * The issuing trait's call-site config. Required so a policy/filter that
25
+ * reads `@config.*` (e.g. std-browse's blank-scope guard
26
+ * `["=", @config.scopeField, ""]`) resolves the knob instead of getting
27
+ * `undefined` — which a bare `=` comparison never treats as "unset", so
28
+ * the guard fails closed and every row is dropped
29
+ * (R-ENTITY-ACCESS-CONFIG-DROPPED-IN-ROW-CTX).
30
+ */
31
+ config?: TraitConfigObject;
32
+ /**
33
+ * Called when a predicate throws. Evaluation still fails closed (the row is
34
+ * dropped / the mutation denied) — this only surfaces the cause, because a
35
+ * fail-closed policy and a genuinely-empty result look identical from the
36
+ * outside, which is exactly how R-ENTITY-ACCESS-CONFIG-DROPPED-IN-ROW-CTX
37
+ * hid for as long as it did.
38
+ */
39
+ onPredicateError?: (error: unknown) => void;
40
+ }
41
+ /** The three directives checked against a row before it is written. */
42
+ type MutationOp = 'create' | 'update' | 'delete';
43
+ /**
44
+ * The one denial message. Three enforcement paths must produce it identically —
45
+ * `OrbitalServerRuntime` (interpreter), `createServerEffectHandlers` (offline
46
+ * preview), and the TypeScript that `orbital-shell-typescript` emits into a
47
+ * generated app's server — because a consumer distinguishing "denied" from
48
+ * "failed" reads this string. The Rust emitter holds a const mirroring it, and
49
+ * a test asserts the two agree.
50
+ */
51
+ declare function accessDeniedMessage(op: MutationOp, entityType: string): string;
52
+ /**
53
+ * Retain only the rows BOTH the declared `@read` policy and the call-site
54
+ * `filter:` accept — the policy can only narrow what the call site sees,
55
+ * never widen it. A no-op when neither is declared.
56
+ *
57
+ * A predicate that fails to evaluate drops its row: a filter/policy that
58
+ * cannot be evaluated must never widen the result set.
59
+ *
60
+ * Generic in the row type so a caller's element type survives the call —
61
+ * generated server code chains a typed `filter:` callback straight off the
62
+ * result, and widening `Ticket[]` to `EntityRow[]` would collapse every field
63
+ * to the index signature and break `tsc` on any arithmetic/date comparison.
64
+ */
65
+ declare function applyRowAccess<T extends EntityRow>(rows: T[], policy: SExpr | undefined, filter: SExpr | undefined, bindings: AccessBindings): T[];
66
+ /**
67
+ * Check a declared `@create`/`@update`/`@delete` directive against the row
68
+ * the operation targets. `row` is the incoming data for `create` (no row
69
+ * exists yet) or the EXISTING row fetched fresh for `update`/`delete`.
70
+ *
71
+ * Returns `true` when no policy is declared (today's behavior, unchanged)
72
+ * or the policy evaluates truthy; `false` denies the mutation, including
73
+ * when the predicate fails to evaluate (fail-closed, same rule as reads).
74
+ */
75
+ declare function checkMutationAccess(row: EntityRow, policy: SExpr | undefined, bindings: AccessBindings): boolean;
76
+
77
+ export { type AccessBindings, type MutationOp, accessDeniedMessage, applyRowAccess, checkMutationAccess };
@@ -0,0 +1,2 @@
1
+ export { accessDeniedMessage, applyRowAccess, checkMutationAccess } from './chunk-XLMDWRMB.js';
2
+ import './chunk-MLKGABMK.js';
package/dist/index.d.ts CHANGED
@@ -4,8 +4,9 @@ import { U as UnifiedLoaderOptions, S as SchemaLoader, I as ImportChainLike, L a
4
4
  export { E as EntitySharingMap, c as EventBus, d as EventNamespaceMap, e as InMemoryPersistence, O as OrbitalEventRequest, f as OrbitalEventResponse, g as OrbitalServerRuntimeConfig, h as PreprocessOptions, i as PreprocessResult, j as PreprocessedSchema, k as ProcessEventOptions, R as RegisteredOrbital, l as RuntimeOrbital, m as RuntimeOrbitalSchema, n as RuntimeTrait, o as StateMachineManager, p as collectDeclaredConfigDefaults, q as collectDeclaredEntityDefaults, r as createInitialTraitState, s as findInitialState, t as findTransition, u as getIsolatedCollectionName, v as getNamespacedEvent, w as isBrowser, x as isElectron, y as isNamespacedEvent, z as isNode, A as normalizeEventKey, B as parseNamespacedEvent, C as preprocessSchema, D as processEvent } from './OrbitalServerRuntime-e-5490xl.js';
5
5
  import { EvaluationContext, SExpressionEvaluator } from '@almadar/evaluator';
6
6
  export { EvaluationContext, createMinimalContext } from '@almadar/evaluator';
7
- import { RenderBindingMarker, SExpr, TraitConfigObject, EventPayload, PatternConfig, EntityId, EntityField, EntityRow, EntityPersistence, ServiceParams, PayloadField, OrbitalDefinition, OrbitalSchema } from '@almadar/core';
7
+ import { RenderBindingMarker, SExpr, TraitConfigObject, EventPayload, PatternConfig, EntityId, EntityField, EntityRow, EntityPersistence, ServiceParams, EntityAccessPolicies, PayloadField, OrbitalDefinition, OrbitalSchema } from '@almadar/core';
8
8
  export { EntityField, normalizeCallSiteConfigToValues } from '@almadar/core';
9
+ export { AccessBindings, applyRowAccess, checkMutationAccess } from './entityAccess.js';
9
10
  export { ServerBridgeConfig, ServerBridgeState } from './ServerBridge.js';
10
11
  export { OsHandlerContext, OsHandlerResult } from './createOsHandlers.js';
11
12
  export { MultiSourceSlotManager, PERF_NAMESPACE, PerfDetail, PerfDetailValue, PerfEntry, PreparedPreviewSchema, RendererContractViolationError, ResolvedPageTraits, SlotContent, SlotContentValidationError, SlotManager, SlotSource, VerificationBus, adjustSchemaForMockData, aggregateSlotContent, assertIsMultiSourceSlotManager, assertIsSlotManager, bindEventBus, bindTraitStateGetter, buildMockData, buildOrbitalsByTrait, clearPerf, collectEmbeddedTraits, collectTraitRefsFromResolvedTrait, createSlotSetter, ensureVerificationApi, getOrbitalVerification, perfEnd, perfStart, perfStore, perfTime, prepareSchemaForPreview, pushPerfEntry, validateSlotContent, wrapCallbackForEvent } from './ui/index.js';
@@ -778,6 +779,16 @@ interface CreateServerEffectHandlersOptions {
778
779
  };
779
780
  /** Consumer-supplied `call-service` handler. When absent, calls warn and return null. */
780
781
  callService?: (service: string, action: string, params?: ServiceParams) => Promise<EventPayload | null>;
782
+ /**
783
+ * The declared `@read`/`@create`/`@update`/`@delete` directives, keyed by
784
+ * entity name. Build it with `entityAccessTable(schema)` from `@almadar/core`.
785
+ *
786
+ * Optional: a caller that holds no schema (the client offline-preview path)
787
+ * passes nothing and gets today's unrestricted behavior. A preview is not a
788
+ * security boundary — the generated server is, and that one always has the
789
+ * policies compiled in.
790
+ */
791
+ entityAccess?: ReadonlyMap<string, EntityAccessPolicies>;
781
792
  /** Verbose logging. */
782
793
  debug?: boolean;
783
794
  }
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
- import { EffectExecutor, createContextFromBindings } from './chunk-FIBEYLME.js';
2
- export { CALLSITE_PAYLOAD_PREFIX, EffectExecutor, EventBus, HANDLER_MANIFEST, InMemoryPersistence, MockPersistenceAdapter, StateMachineManager, TickScheduler, buildEmitsFromTraits, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, containsBindings, createContextFromBindings, createInitialTraitState, createMinimalContext, createMockPersistence, createTestExecutor, createTickScheduler, createUnifiedLoader, deferEntityBindings, extractBindings, findInitialState, findTransition, formatPayloadValidationError, getIsolatedCollectionName, getNamespacedEvent, interpolateProps, interpolateValue, isBrowser, isElectron, isNamespacedEvent, isNode, isValidDurationString, normalizeCallSiteConfigToValues, normalizeEventKey, parseDurationString, parseNamespacedEvent, preprocessSchema, processEvent, resolveCallSitePayloadCaptures, validateEventPayload, validatePayloadShapes } from './chunk-FIBEYLME.js';
1
+ import { EffectExecutor } from './chunk-ML75GCRO.js';
2
+ export { EffectExecutor, EventBus, HANDLER_MANIFEST, InMemoryPersistence, MockPersistenceAdapter, StateMachineManager, TickScheduler, buildEmitsFromTraits, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, createInitialTraitState, createMockPersistence, createTestExecutor, createTickScheduler, createUnifiedLoader, findInitialState, findTransition, formatPayloadValidationError, getIsolatedCollectionName, getNamespacedEvent, isBrowser, isElectron, isNamespacedEvent, isNode, isValidDurationString, normalizeCallSiteConfigToValues, normalizeEventKey, parseDurationString, parseNamespacedEvent, preprocessSchema, processEvent, validateEventPayload, validatePayloadShapes } from './chunk-ML75GCRO.js';
3
3
  export { cronMatches, cronMinuteKey, isValidCronExpression, parseCron, parseCronField } from './chunk-OU3ITB5S.js';
4
+ import { createContextFromBindings, applyRowAccess, checkMutationAccess, accessDeniedMessage } from './chunk-XLMDWRMB.js';
5
+ export { CALLSITE_PAYLOAD_PREFIX, applyRowAccess, checkMutationAccess, containsBindings, createContextFromBindings, createMinimalContext, deferEntityBindings, extractBindings, interpolateProps, interpolateValue, resolveCallSitePayloadCaptures } from './chunk-XLMDWRMB.js';
4
6
  import './chunk-T4VDAB4C.js';
5
7
  export { PERF_NAMESPACE, RendererContractViolationError, adjustSchemaForMockData, aggregateSlotContent, assertIsMultiSourceSlotManager, assertIsSlotManager, bindEventBus, bindTraitStateGetter, buildMockData, buildOrbitalsByTrait, clearPerf, createSlotSetter, ensureVerificationApi, getOrbitalVerification, perfEnd, perfStart, perfStore, perfTime, prepareSchemaForPreview, pushPerfEntry, validateSlotContent, wrapCallbackForEvent } from './chunk-FOFLEZRJ.js';
6
8
  export { collectEmbeddedTraits, collectTraitRefsFromResolvedTrait } from './chunk-SCRAHWOC.js';
@@ -154,8 +156,20 @@ function createServerEffectHandlers(opts) {
154
156
  emittedEvents,
155
157
  source,
156
158
  callService: consumerCallService,
159
+ entityAccess,
157
160
  debug
158
161
  } = opts;
162
+ const accessBindings = (forEntityType) => ({
163
+ user: bindings?.user,
164
+ payload: bindings?.payload,
165
+ config: bindings?.config,
166
+ onPredicateError: (err) => {
167
+ effectLog.error("access-predicate-eval-error", {
168
+ entityType: forEntityType,
169
+ error: err instanceof Error ? err : String(err)
170
+ });
171
+ }
172
+ });
159
173
  const record = (entry) => {
160
174
  effectResults?.push(entry);
161
175
  };
@@ -306,8 +320,12 @@ function createServerEffectHandlers(opts) {
306
320
  let resultData;
307
321
  const sizeBefore = (await persistence.list(type)).length;
308
322
  try {
323
+ const mutationPolicy = entityAccess?.get(type)?.[action];
309
324
  switch (action) {
310
325
  case "create": {
326
+ if (!checkMutationAccess(data ?? {}, mutationPolicy, accessBindings(type))) {
327
+ throw new Error(accessDeniedMessage("create", type));
328
+ }
311
329
  const { id } = await persistence.create(type, data ?? {});
312
330
  resultData = { id, ...data ?? {} };
313
331
  break;
@@ -316,6 +334,12 @@ function createServerEffectHandlers(opts) {
316
334
  const row = data ?? {};
317
335
  const idOrFallback = row.id ?? entityId;
318
336
  if (idOrFallback) {
337
+ if (mutationPolicy !== void 0) {
338
+ const existing = await persistence.getById(type, idOrFallback);
339
+ if (!existing || !checkMutationAccess(existing, mutationPolicy, accessBindings(type))) {
340
+ throw new Error(accessDeniedMessage("update", type));
341
+ }
342
+ }
319
343
  await persistence.update(type, idOrFallback, row);
320
344
  const updated = await persistence.getById(type, idOrFallback);
321
345
  resultData = updated ?? { id: idOrFallback, ...row };
@@ -327,6 +351,12 @@ function createServerEffectHandlers(opts) {
327
351
  const nestedId = typeof data === "object" && data !== null ? data.id : void 0;
328
352
  const deleteId = directId ?? nestedId ?? entityId;
329
353
  if (deleteId) {
354
+ if (mutationPolicy !== void 0) {
355
+ const existing = await persistence.getById(type, deleteId);
356
+ if (!existing || !checkMutationAccess(existing, mutationPolicy, accessBindings(type))) {
357
+ throw new Error(accessDeniedMessage("delete", type));
358
+ }
359
+ }
330
360
  await persistence.delete(type, deleteId);
331
361
  resultData = { id: deleteId, deleted: true };
332
362
  }
@@ -413,31 +443,25 @@ function createServerEffectHandlers(opts) {
413
443
  let total = 0;
414
444
  if (options?.id) {
415
445
  const entity = await persistence.getById(fetchEntityType, options.id);
416
- if (entity) {
417
- if (fetchedData) fetchedData[fetchEntityType] = [entity];
418
- result = entity;
446
+ const visible = entity ? applyRowAccess(
447
+ [entity],
448
+ entityAccess?.get(fetchEntityType)?.read,
449
+ void 0,
450
+ accessBindings(fetchEntityType)
451
+ ) : [];
452
+ if (visible.length > 0) {
453
+ if (fetchedData) fetchedData[fetchEntityType] = visible;
454
+ result = visible[0] ?? null;
419
455
  total = 1;
420
456
  }
421
457
  } else {
422
458
  let entities = await persistence.list(fetchEntityType);
423
- if (options?.filter !== void 0 && options.filter !== null) {
424
- const predicate = options.filter;
425
- entities = entities.filter((entity) => {
426
- const ctx = createContextFromBindings(
427
- { entity, payload: bindings?.payload, current: entity, user: bindings?.user, config: bindings?.config },
428
- false
429
- );
430
- try {
431
- return Boolean(evaluate(predicate, ctx));
432
- } catch (err) {
433
- effectLog.error("fetch-filter-eval-error", {
434
- entityType: fetchEntityType,
435
- error: err instanceof Error ? err : String(err)
436
- });
437
- return false;
438
- }
439
- });
440
- }
459
+ entities = applyRowAccess(
460
+ entities,
461
+ entityAccess?.get(fetchEntityType)?.read,
462
+ options?.filter,
463
+ accessBindings(fetchEntityType)
464
+ );
441
465
  total = entities.length;
442
466
  if (options?.offset && options.offset > 0) {
443
467
  entities = entities.slice(options.offset);
@@ -460,7 +484,13 @@ function createServerEffectHandlers(opts) {
460
484
  fetchStream: async (streamEntityType, options, onChunk) => {
461
485
  try {
462
486
  const rows = await persistence.list(streamEntityType);
463
- const matched = options?.id ? rows.filter((r) => r["id"] === options.id) : rows;
487
+ const visible = applyRowAccess(
488
+ rows,
489
+ entityAccess?.get(streamEntityType)?.read,
490
+ void 0,
491
+ accessBindings(streamEntityType)
492
+ );
493
+ const matched = options?.id ? visible.filter((r) => r["id"] === options.id) : visible;
464
494
  for (const row of matched) {
465
495
  onChunk(row);
466
496
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/runtime",
3
- "version": "6.47.0",
3
+ "version": "6.49.0",
4
4
  "description": "Interpreted runtime for Almadar orbital applications (OrbitalServerRuntime)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,6 +33,11 @@
33
33
  "import": "./dist/createOsHandlers.js",
34
34
  "require": "./dist/createOsHandlers.js"
35
35
  },
36
+ "./entityAccess": {
37
+ "types": "./dist/entityAccess.d.ts",
38
+ "import": "./dist/entityAccess.js",
39
+ "require": "./dist/entityAccess.js"
40
+ },
36
41
  "./mockRandom": {
37
42
  "types": "./dist/mockRandom.d.ts",
38
43
  "import": "./dist/mockRandom.js",
@@ -52,11 +57,11 @@
52
57
  "access": "public"
53
58
  },
54
59
  "dependencies": {
55
- "@almadar/core": "^10.45.0",
56
- "@almadar/evaluator": "^2.38.0",
57
- "@almadar/logger": "^1.10.0",
58
- "@almadar/server": "^2.30.0",
59
- "@almadar/std": "^16.155.0"
60
+ "@almadar/core": "^10.49.0",
61
+ "@almadar/evaluator": "^2.39.0",
62
+ "@almadar/logger": "^1.11.0",
63
+ "@almadar/server": "^2.33.0",
64
+ "@almadar/std": "^16.160.0"
60
65
  },
61
66
  "peerDependencies": {
62
67
  "express": "^5.0.0"
@@ -70,7 +75,7 @@
70
75
  "@almadar/eslint-plugin": "^2.15.0",
71
76
  "@types/express": "^5.0.0",
72
77
  "@types/node": "^20.0.0",
73
- "@typescript-eslint/parser": "8.56.0",
78
+ "@typescript-eslint/parser": "8.65.0",
74
79
  "eslint": "10.0.0",
75
80
  "express": "^5.0.0",
76
81
  "tsup": "^8.0.0",