@almadar/runtime 6.46.0 → 6.48.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/dist/OrbitalServerRuntime.js +30 -22
- package/dist/{chunk-FIBEYLME.js → chunk-ML75GCRO.js} +4 -291
- package/dist/chunk-XLMDWRMB.js +335 -0
- package/dist/entityAccess.d.ts +77 -0
- package/dist/entityAccess.js +2 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +54 -24
- package/package.json +9 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createTickScheduler, EventBus, createUnifiedLoader, MockPersistenceAdapter, InMemoryPersistence, preprocessSchema, normalizeCallSiteConfigToValues, StateMachineManager, parseDurationString,
|
|
2
|
-
export { InMemoryPersistence, collectDeclaredConfigDefaults } from './chunk-
|
|
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';
|
|
@@ -8,7 +9,7 @@ import { createLogger } from '@almadar/logger';
|
|
|
8
9
|
import * as nodeModule from 'module';
|
|
9
10
|
import { evaluateListenPayloadExpr, evaluateGuard, evaluate } from '@almadar/evaluator';
|
|
10
11
|
import { DEFAULT_VIEWER, buildResolvedTraitConfigs, isInlineTrait, isEntityCall, applyListenPayloadMapping, personaFromIdentityRow, normalizeUserContext, isRuntimeEntity } from '@almadar/core';
|
|
11
|
-
import { ownerFieldsFromSchema, identityEntityName } from '@almadar/core/mock';
|
|
12
|
+
import { ownerFieldsFromSchema, identityEntityName, entityAccessPolicies } from '@almadar/core/mock';
|
|
12
13
|
|
|
13
14
|
// src/identity/routing.ts
|
|
14
15
|
function eventRouteKey(eventName, eventId) {
|
|
@@ -1316,8 +1317,13 @@ var OrbitalServerRuntime = class {
|
|
|
1316
1317
|
if (action === "create" || action === "update") {
|
|
1317
1318
|
this.validateRelationCardinality(type, data || {});
|
|
1318
1319
|
}
|
|
1320
|
+
const accessBindings = { user: bindingsRef?.user, payload: bindingsRef?.payload, config: bindingsRef?.config };
|
|
1321
|
+
const mutationPolicy = this.resolvedSchema ? entityAccessPolicies(this.resolvedSchema, type)?.[action === "create" ? "create" : action === "update" ? "update" : "delete"] : void 0;
|
|
1319
1322
|
switch (action) {
|
|
1320
1323
|
case "create": {
|
|
1324
|
+
if (!checkMutationAccess(data || {}, mutationPolicy, accessBindings)) {
|
|
1325
|
+
throw new Error(accessDeniedMessage("create", type));
|
|
1326
|
+
}
|
|
1321
1327
|
const { id } = await this.persistence.create(type, data || {});
|
|
1322
1328
|
resultData = { id, ...data || {} };
|
|
1323
1329
|
break;
|
|
@@ -1325,6 +1331,12 @@ var OrbitalServerRuntime = class {
|
|
|
1325
1331
|
case "update":
|
|
1326
1332
|
if (data?.id || entityId) {
|
|
1327
1333
|
const updateId = data?.id || entityId;
|
|
1334
|
+
if (mutationPolicy !== void 0) {
|
|
1335
|
+
const existing = await this.persistence.getById(type, updateId);
|
|
1336
|
+
if (!existing || !checkMutationAccess(existing, mutationPolicy, accessBindings)) {
|
|
1337
|
+
throw new Error(accessDeniedMessage("update", type));
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1328
1340
|
await this.persistence.update(type, updateId, data || {});
|
|
1329
1341
|
const updated = await this.persistence.getById(type, updateId);
|
|
1330
1342
|
resultData = updated || { id: updateId, ...data || {} };
|
|
@@ -1335,6 +1347,12 @@ var OrbitalServerRuntime = class {
|
|
|
1335
1347
|
const nestedId = typeof data === "object" && data !== null ? data.id : void 0;
|
|
1336
1348
|
const deleteId = directId ?? nestedId ?? entityId;
|
|
1337
1349
|
if (deleteId) {
|
|
1350
|
+
if (mutationPolicy !== void 0) {
|
|
1351
|
+
const existing = await this.persistence.getById(type, deleteId);
|
|
1352
|
+
if (!existing || !checkMutationAccess(existing, mutationPolicy, accessBindings)) {
|
|
1353
|
+
throw new Error(accessDeniedMessage("delete", type));
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1338
1356
|
await this.enforceOnDeleteRules(type, deleteId);
|
|
1339
1357
|
await this.persistence.delete(type, deleteId);
|
|
1340
1358
|
resultData = { id: deleteId, deleted: true };
|
|
@@ -1423,9 +1441,11 @@ var OrbitalServerRuntime = class {
|
|
|
1423
1441
|
try {
|
|
1424
1442
|
let result = null;
|
|
1425
1443
|
let total = 0;
|
|
1444
|
+
const readPolicy = this.resolvedSchema ? entityAccessPolicies(this.resolvedSchema, fetchEntityType)?.read : void 0;
|
|
1445
|
+
const accessBindings = { user: bindingsRef?.user, payload: bindingsRef?.payload, config: bindingsRef?.config };
|
|
1426
1446
|
if (options?.id) {
|
|
1427
1447
|
const entity = await this.persistence.getById(fetchEntityType, options.id);
|
|
1428
|
-
if (entity) {
|
|
1448
|
+
if (entity && applyRowAccess([entity], readPolicy, void 0, accessBindings).length > 0) {
|
|
1429
1449
|
if (options?.include && options.include.length > 0) {
|
|
1430
1450
|
await this.populateRelations([entity], fetchEntityType, options.include);
|
|
1431
1451
|
}
|
|
@@ -1435,24 +1455,12 @@ var OrbitalServerRuntime = class {
|
|
|
1435
1455
|
}
|
|
1436
1456
|
} else {
|
|
1437
1457
|
let entities = await this.persistence.list(fetchEntityType);
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
);
|
|
1445
|
-
try {
|
|
1446
|
-
return Boolean(evaluate(predicate, ctx));
|
|
1447
|
-
} catch (err) {
|
|
1448
|
-
effectLog.error("fetch:filter-eval-error", {
|
|
1449
|
-
entityType: fetchEntityType,
|
|
1450
|
-
error: err instanceof Error ? err : String(err)
|
|
1451
|
-
});
|
|
1452
|
-
return false;
|
|
1453
|
-
}
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1458
|
+
entities = applyRowAccess(
|
|
1459
|
+
entities,
|
|
1460
|
+
readPolicy,
|
|
1461
|
+
options?.filter !== void 0 && options.filter !== null ? options.filter : void 0,
|
|
1462
|
+
accessBindings
|
|
1463
|
+
);
|
|
1456
1464
|
total = entities.length;
|
|
1457
1465
|
if (options?.offset && options.offset > 0) {
|
|
1458
1466
|
entities = entities.slice(options.offset);
|
|
@@ -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 {
|
|
6
|
-
|
|
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 {
|
|
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 };
|
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
|
|
2
|
-
export {
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "6.48.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.
|
|
60
|
+
"@almadar/core": "^10.47.0",
|
|
56
61
|
"@almadar/evaluator": "^2.38.0",
|
|
57
62
|
"@almadar/logger": "^1.10.0",
|
|
58
|
-
"@almadar/server": "^2.
|
|
59
|
-
"@almadar/std": "^16.
|
|
63
|
+
"@almadar/server": "^2.32.0",
|
|
64
|
+
"@almadar/std": "^16.157.0"
|
|
60
65
|
},
|
|
61
66
|
"peerDependencies": {
|
|
62
67
|
"express": "^5.0.0"
|