@web-ts-toolkit/access-router 0.53.0 → 0.55.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/advanced.mjs CHANGED
@@ -41,7 +41,7 @@ var FilterOperator = /* @__PURE__ */ ((FilterOperator2) => {
41
41
  })(FilterOperator || {});
42
42
 
43
43
  // src/validation/parsers.ts
44
- import JsonRouter2 from "@web-ts-toolkit/express-json-router";
44
+ import JsonRouter3 from "@web-ts-toolkit/express-json-router";
45
45
 
46
46
  // src/runtime.ts
47
47
  import mongoose from "mongoose";
@@ -87,10 +87,10 @@ function assertPublicAdmission(input, kind = "envelope") {
87
87
  const add = (child, childKind, key) => {
88
88
  pending.push({ value: child, kind: childKind, path: `${path}.${key}` });
89
89
  };
90
- const record = value;
90
+ const record2 = value;
91
91
  if (slot !== "data") {
92
92
  for (const key of privateControls) {
93
- if (key in record) {
93
+ if (key in record2) {
94
94
  throw new PublicAdmissionError(
95
95
  `Unsupported public control ${path}.${key}; use explicit per-call internal service options for backend authorization`
96
96
  );
@@ -106,16 +106,16 @@ function assertPublicAdmission(input, kind = "envelope") {
106
106
  for (const [key, child] of Object.entries(value)) add(child, key === "$$sq" ? "envelope" : "data", key);
107
107
  continue;
108
108
  }
109
- for (const key of ["args", "options", "sqOptions"]) add(record[key], "envelope", key);
110
- add(record.include, "include", "include");
111
- add(record.populate, "populate", "populate");
112
- for (const key of ["filter", "match", "data"]) add(record[key], "data", key);
109
+ for (const key of ["args", "options", "sqOptions"]) add(record2[key], "envelope", key);
110
+ add(record2.include, "include", "include");
111
+ add(record2.populate, "populate", "populate");
112
+ for (const key of ["filter", "match", "data"]) add(record2[key], "data", key);
113
113
  }
114
114
  }
115
115
  function assertHydrationDefaults(defaults) {
116
116
  if (defaults && typeof defaults === "object") {
117
- const record = defaults;
118
- if ("hydration" in record)
117
+ const record2 = defaults;
118
+ if ("hydration" in record2)
119
119
  throw new TypeError("defaults.hydration is unsupported; use model hydration configuration");
120
120
  for (const bucket of [
121
121
  "publicListOptions",
@@ -131,7 +131,7 @@ function assertHydrationDefaults(defaults) {
131
131
  "publicCreateOptions",
132
132
  "publicUpdateOptions"
133
133
  ]) {
134
- const options = record[bucket];
134
+ const options = record2[bucket];
135
135
  if (!options || typeof options !== "object" && typeof options !== "function") continue;
136
136
  for (const key of ["lean", "hydration"]) {
137
137
  if (!(key in options)) continue;
@@ -181,12 +181,12 @@ import mschema2Jsonschema from "mongoose-schema-jsonschema";
181
181
  import {
182
182
  addLeadingSlash,
183
183
  forEach as forEach2,
184
- get as get2,
184
+ get as get3,
185
185
  isArray as isArray4,
186
186
  isBoolean,
187
- isFunction,
187
+ isFunction as isFunction2,
188
188
  isNil as isNil2,
189
- isPlainObject as isPlainObject10,
189
+ isPlainObject as isPlainObject12,
190
190
  isString as isString3,
191
191
  keys,
192
192
  normalizeUrlPath,
@@ -196,6 +196,23 @@ import {
196
196
  // src/helpers/query.ts
197
197
  import { flattenDeep, isNil, isPlainObject as isPlainObject2, isString, reduce } from "@web-ts-toolkit/utils";
198
198
  var DEFAULT_LIST_HARD_LIMIT = 1e3;
199
+ function normalizeSelect(select) {
200
+ if (Array.isArray(select)) return flattenDeep(select.map(normalizeSelect));
201
+ if (isPlainObject2(select)) {
202
+ return reduce(
203
+ select,
204
+ (ret, val, key) => {
205
+ if (val === 1) ret.push(key);
206
+ else if (val === -1) ret.push(`-${key}`);
207
+ return ret;
208
+ },
209
+ []
210
+ );
211
+ }
212
+ if (isString(select))
213
+ return String(select).split(/[\s,]+/).map((v) => v.trim()).filter(Boolean);
214
+ return [];
215
+ }
199
216
 
200
217
  // src/helpers/sort-policy.ts
201
218
  var fieldPathPattern = /^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)*$/;
@@ -1111,6 +1128,482 @@ function validateRequestComplexity(value, options, scope = "request") {
1111
1128
  return errors;
1112
1129
  }
1113
1130
 
1131
+ // src/acl/virtual-descriptor.ts
1132
+ import { isFunction, isPlainObject as isPlainObject10 } from "@web-ts-toolkit/utils";
1133
+ var isVirtualContainerValue = (value) => isPlainObject10(value) && "sub" in value;
1134
+ function getScopeVirtualsObject(root, scopePath = []) {
1135
+ let current = root;
1136
+ for (let i = 0; i < scopePath.length; i += 2) {
1137
+ if (!current || scopePath[i + 1] !== "sub") return null;
1138
+ const container = current[scopePath[i]];
1139
+ if (!isVirtualContainerValue(container)) return null;
1140
+ current = container.sub;
1141
+ }
1142
+ return isPlainObject10(current) ? current : null;
1143
+ }
1144
+ function toDescriptor(leaf, shared = {}) {
1145
+ const record2 = isFunction(leaf) ? { get: leaf } : isPlainObject10(leaf) ? leaf : null;
1146
+ if (!record2 || !isFunction(record2.get)) return void 0;
1147
+ const dependsOn = record2.dependsOn ?? shared.dependsOn ?? [];
1148
+ const populated = record2.dependsOnPopulate === void 0 ? shared.dependsOnPopulate : record2.dependsOnPopulate;
1149
+ return {
1150
+ get: record2.get,
1151
+ dependsOn: [...dependsOn],
1152
+ ...populated === void 0 ? {} : {
1153
+ dependsOnPopulate: populated.map(({ path, select }) => ({
1154
+ path,
1155
+ ...select === void 0 ? {} : { select: Array.isArray(select) ? [...select] : typeof select === "object" ? { ...select } : select }
1156
+ }))
1157
+ }
1158
+ };
1159
+ }
1160
+ function resolveSnapshotDescriptor(scopeVirtuals, virtualName, access) {
1161
+ const entry = scopeVirtuals?.[virtualName];
1162
+ if (entry === void 0 || isVirtualContainerValue(entry)) return void 0;
1163
+ if (isFunction(entry)) return toDescriptor(entry);
1164
+ if (!isPlainObject10(entry)) return void 0;
1165
+ const record2 = entry;
1166
+ if ("get" in record2) return toDescriptor(record2);
1167
+ return toDescriptor(record2[access], record2) ?? toDescriptor(record2.default, record2);
1168
+ }
1169
+
1170
+ // src/acl/virtual-populate-dependencies.ts
1171
+ import { get as get2, isPlainObject as isPlainObject11 } from "@web-ts-toolkit/utils";
1172
+
1173
+ // src/acl/authorization-snapshot.ts
1174
+ import { isDeepStrictEqual } from "util";
1175
+ import { mongo } from "mongoose";
1176
+ import JsonRouter2 from "@web-ts-toolkit/express-json-router";
1177
+ var ServiceAuthorizationError = class extends JsonRouter2.clientErrors.BadRequestError {
1178
+ constructor(detail) {
1179
+ const errors = [{ detail: `Service authorization: ${detail}` }];
1180
+ super("Bad Request", { errors });
1181
+ this.result = { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors };
1182
+ }
1183
+ };
1184
+ function authorizationError(detail) {
1185
+ throw new ServiceAuthorizationError(detail);
1186
+ }
1187
+ var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1188
+ function authorizationRecord(value, label) {
1189
+ if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
1190
+ authorizationError(`${label} must be a plain object`);
1191
+ }
1192
+ return value;
1193
+ }
1194
+ function copyAuthorizationValue(value, configuration = false) {
1195
+ const active = /* @__PURE__ */ new WeakSet();
1196
+ let nodes = 0;
1197
+ function copy(input, depth) {
1198
+ if (++nodes > 2e4 || depth > 100) authorizationError("snapshot exceeds structural limits");
1199
+ if (typeof input === "function") {
1200
+ if (configuration === true) return input;
1201
+ authorizationError("functions are not policy values");
1202
+ }
1203
+ if (typeof input === "symbol") authorizationError("symbols are not policy values");
1204
+ if (input === null || typeof input !== "object") return input;
1205
+ if (active.has(input)) authorizationError("cyclic input");
1206
+ active.add(input);
1207
+ try {
1208
+ if (input instanceof Date) return new Date(input.getTime());
1209
+ if (Buffer.isBuffer(input)) return Buffer.from(input);
1210
+ if (input instanceof RegExp) {
1211
+ const result2 = new RegExp(input.source, input.flags);
1212
+ result2.lastIndex = input.lastIndex;
1213
+ return result2;
1214
+ }
1215
+ if (input instanceof mongo.BSON.BSONValue) {
1216
+ try {
1217
+ return mongo.BSON.deserialize(mongo.BSON.serialize({ value: input }), {
1218
+ promoteValues: false,
1219
+ bsonRegExp: true
1220
+ }).value;
1221
+ } catch {
1222
+ authorizationError("unsupported BSON snapshot");
1223
+ }
1224
+ }
1225
+ if (configuration && input instanceof Map) {
1226
+ return new Map([...input].map(([key, entry]) => [copy(key, depth + 1), copy(entry, depth + 1)]));
1227
+ }
1228
+ if (configuration && input instanceof Set) return new Set([...input].map((entry) => copy(entry, depth + 1)));
1229
+ const array = Array.isArray(input);
1230
+ if (array && input.length > 2e4) authorizationError("snapshot array exceeds structural limits");
1231
+ if (!array) authorizationRecord(input, "snapshot");
1232
+ const result = array ? [] : {};
1233
+ for (const key of Reflect.ownKeys(input)) {
1234
+ if (array && key === "length") continue;
1235
+ if (typeof key !== "string" || unsafeKeys.has(key)) authorizationError("unsafe snapshot key");
1236
+ if (array && !/^(0|[1-9]\d*)$/.test(key)) authorizationError("invalid array property");
1237
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
1238
+ if (!("value" in descriptor) || !descriptor.enumerable && configuration !== "document")
1239
+ authorizationError("accessor/non-enumerable input");
1240
+ Object.defineProperty(result, key, {
1241
+ value: copy(descriptor.value, depth + 1),
1242
+ enumerable: true,
1243
+ writable: true,
1244
+ configurable: true
1245
+ });
1246
+ }
1247
+ if (array) result.length = input.length;
1248
+ return Object.freeze(result);
1249
+ } finally {
1250
+ active.delete(input);
1251
+ }
1252
+ }
1253
+ return copy(value, 0);
1254
+ }
1255
+
1256
+ // src/acl/field-catalog.ts
1257
+ var safeSegment = (part) => !!part && part !== "sub" && !["__proto__", "constructor", "prototype", "_doc", "__v"].includes(part) && !/^[+$-]|[\s*]/.test(part) && !part.includes("[") && !part.includes("]") && !/^\d+$/.test(part) && ![...part].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
1258
+ var isSafeOutputPath = (path) => path.split(".").every(safeSegment);
1259
+ var inputControlSegments = /* @__PURE__ */ new Set([
1260
+ "_permissions",
1261
+ "_view",
1262
+ "_edit",
1263
+ "documentAuthorization",
1264
+ "documentAuthorizationStage",
1265
+ "resolvedAuthorization",
1266
+ "execution",
1267
+ "executionContext",
1268
+ "serviceExecutionContext",
1269
+ "preparedQuery",
1270
+ "preparedFilter",
1271
+ "preparedModelQuery",
1272
+ "resolvedIdentifier",
1273
+ "skipParsing",
1274
+ "skipAuthorization",
1275
+ "alreadyAuthorized"
1276
+ ]);
1277
+ function isSafeInputPath(path, requestIdentity = false) {
1278
+ return isSafeOutputPath(path) && path.split(".").every((part) => !inputControlSegments.has(part) && (requestIdentity || part !== "_id"));
1279
+ }
1280
+ function matchesCatalogPattern(pattern, path, descendants = false) {
1281
+ const expected = pattern.split(".");
1282
+ const actual = path.split(".");
1283
+ return (descendants ? actual.length >= expected.length : actual.length === expected.length) && expected.every((part, index) => part === "$*" || part === actual[index]);
1284
+ }
1285
+ function captureAuthorizationFieldCatalog(schema, virtuals, documentPermissionField = "_permissions") {
1286
+ const paths = /* @__PURE__ */ new Set();
1287
+ const containers = /* @__PURE__ */ new Set();
1288
+ const opaque = /* @__PURE__ */ new Set();
1289
+ const references = /* @__PURE__ */ new Set();
1290
+ const virtualPaths = /* @__PURE__ */ new Set();
1291
+ const virtualScopes = /* @__PURE__ */ new Set();
1292
+ const schemaVirtualPaths = /* @__PURE__ */ new Set();
1293
+ const arrays = /* @__PURE__ */ new Set();
1294
+ const protectedPaths = /* @__PURE__ */ new Set([documentPermissionField, "_doc", "$__", "__v"]);
1295
+ const add = (path) => {
1296
+ if (path.split(".").every((part) => part === "$*" || safeSegment(part))) paths.add(path);
1297
+ };
1298
+ const ancestors = (path) => {
1299
+ const parts = path.split(".");
1300
+ for (let n = 1; n < parts.length; n++) {
1301
+ const parent = parts.slice(0, n).join(".");
1302
+ add(parent);
1303
+ containers.add(parent);
1304
+ }
1305
+ };
1306
+ const visitType = (type, path, active) => {
1307
+ add(path);
1308
+ ancestors(path);
1309
+ const element = type.$embeddedSchemaType ?? type.embeddedSchemaType ?? type.caster;
1310
+ if (type.instance === "Array" || type.$isMongooseDocumentArray) arrays.add(path);
1311
+ if (type.options?.ref || type.options?.refPath || element?.options?.ref || element?.options?.refPath) {
1312
+ references.add(path);
1313
+ return;
1314
+ }
1315
+ if (type.$isSchemaMap || type.instance === "Map") {
1316
+ const value = type.$__schemaType;
1317
+ if (value) visitType(value, `${path}.$*`, active);
1318
+ else opaque.add(path);
1319
+ containers.delete(path);
1320
+ return;
1321
+ }
1322
+ const child = type.schema ?? element?.schema;
1323
+ if (child) {
1324
+ containers.add(path);
1325
+ if (!active.has(child)) visitSchema(child, path, /* @__PURE__ */ new Set([...active, child]));
1326
+ return;
1327
+ }
1328
+ if (type.instance === "Mixed" || element?.instance === "Mixed") opaque.add(path);
1329
+ };
1330
+ const visitSchema = (current, prefix, active) => {
1331
+ const version = current.options.versionKey;
1332
+ if (typeof version === "string") protectedPaths.add(prefix ? `${prefix}.${version}` : version);
1333
+ for (const name of Object.keys(current.virtuals)) {
1334
+ schemaVirtualPaths.add(prefix ? `${prefix}.${name}` : name);
1335
+ }
1336
+ for (const [name, raw] of Object.entries(current.paths)) {
1337
+ if (name.includes("$*")) continue;
1338
+ visitType(raw, prefix ? `${prefix}.${name}` : name, active);
1339
+ }
1340
+ };
1341
+ visitSchema(schema, "", /* @__PURE__ */ new Set([schema]));
1342
+ const persistedPaths = [...paths];
1343
+ const visitVirtuals = (record2, prefix) => {
1344
+ for (const [name, value] of Object.entries(record2 ?? {})) {
1345
+ if (value === void 0) continue;
1346
+ const path = prefix ? `${prefix}.${name}` : name;
1347
+ if (!isSafeOutputPath(path)) continue;
1348
+ if (value && typeof value === "object" && "sub" in value) {
1349
+ if (references.has(path)) continue;
1350
+ virtualScopes.add(path);
1351
+ add(path);
1352
+ containers.add(path);
1353
+ visitVirtuals(value.sub, path);
1354
+ } else {
1355
+ add(path);
1356
+ ancestors(path);
1357
+ virtualPaths.add(path);
1358
+ }
1359
+ }
1360
+ };
1361
+ visitVirtuals(virtuals, "");
1362
+ const eligible = [...paths].filter((p) => ![...protectedPaths].some((v) => matchesCatalogPattern(v, p, true)));
1363
+ return Object.freeze({
1364
+ paths: Object.freeze(eligible.filter((p) => !p.includes("$*"))),
1365
+ persistedPaths: Object.freeze(persistedPaths),
1366
+ arrayPaths: Object.freeze([...arrays]),
1367
+ schemaVirtualPaths: Object.freeze([...schemaVirtualPaths]),
1368
+ metadataPaths: Object.freeze([documentPermissionField]),
1369
+ dynamicPaths: Object.freeze(eligible.filter((p) => p.includes("$*"))),
1370
+ containerPaths: Object.freeze([...containers]),
1371
+ opaquePaths: Object.freeze([...opaque]),
1372
+ referencePaths: Object.freeze([...references]),
1373
+ virtualPaths: Object.freeze([...virtualPaths]),
1374
+ virtualScopePaths: Object.freeze([...virtualScopes]),
1375
+ protectedPaths: Object.freeze([...protectedPaths])
1376
+ });
1377
+ }
1378
+
1379
+ // src/acl/virtual-populate-dependencies.ts
1380
+ var elementType = (type) => type.$embeddedSchemaType ?? type.embeddedSchemaType ?? type.caster;
1381
+ var join = (prefix, path) => prefix ? `${prefix}.${path}` : path;
1382
+ var within = (path, parent) => path === parent || path.startsWith(`${parent}.`);
1383
+ var accesses = ["default", "list", "create", "read", "update"];
1384
+ var fail = (label, message) => {
1385
+ throw new Error(`Virtual ${label}: dependsOnPopulate ${message}`);
1386
+ };
1387
+ var varyingSchema = (schema) => Object.keys(schema.discriminators ?? {}).length > 0 || schema.discriminatorMapping?.isRoot === false;
1388
+ var varyingModel = (model) => !!model.baseModelName || Object.keys(model.discriminators ?? {}).length > 0 || varyingSchema(model.schema);
1389
+ function record(value, label) {
1390
+ if (!isPlainObject11(value)) return fail(label, "entries must be plain { path, select? } objects");
1391
+ for (const key of Reflect.ownKeys(value)) {
1392
+ const property = Object.getOwnPropertyDescriptor(value, key);
1393
+ if (typeof key !== "string" || !property.enumerable || !("value" in property))
1394
+ fail(label, "requires enumerable data properties");
1395
+ }
1396
+ return value;
1397
+ }
1398
+ function targetSelectionCatalog(schema, options) {
1399
+ const leaves = /* @__PURE__ */ new Set();
1400
+ const containers = /* @__PURE__ */ new Set();
1401
+ const forbidden = /* @__PURE__ */ new Set([options.documentPermissionField ?? "_permissions"]);
1402
+ const visit = (current, prefix, active) => {
1403
+ const version = current.options.versionKey;
1404
+ if (typeof version === "string") forbidden.add(join(prefix, version));
1405
+ for (const name of Object.keys(current.virtuals)) forbidden.add(join(prefix, name));
1406
+ for (const name of Object.keys(current.aliases ?? {})) forbidden.add(join(prefix, name));
1407
+ for (const [name, raw] of Object.entries(current.paths)) {
1408
+ const path = join(prefix, name);
1409
+ if (!isSafeInputPath(path, true)) continue;
1410
+ const parts = path.split(".");
1411
+ for (let n = 1; n < parts.length; n++) containers.add(parts.slice(0, n).join("."));
1412
+ let type = raw;
1413
+ while (type.instance === "Array" && elementType(type)) type = elementType(type);
1414
+ if (type.instance === "Mixed" || type.instance === "Map") {
1415
+ forbidden.add(path);
1416
+ continue;
1417
+ }
1418
+ const child = type.schema ?? raw.schema;
1419
+ if (raw.options?.ref !== void 0 || raw.options?.refPath !== void 0 || type.options?.ref !== void 0 || type.options?.refPath !== void 0) {
1420
+ if (child) forbidden.add(path);
1421
+ else leaves.add(path);
1422
+ continue;
1423
+ }
1424
+ if (child) {
1425
+ if (active.has(child) || varyingSchema(child)) forbidden.add(path);
1426
+ else {
1427
+ containers.add(path);
1428
+ visit(child, path, /* @__PURE__ */ new Set([...active, child]));
1429
+ }
1430
+ } else leaves.add(path);
1431
+ }
1432
+ };
1433
+ visit(schema, "", /* @__PURE__ */ new Set([schema]));
1434
+ const visitVirtuals = (virtuals, prefix) => {
1435
+ for (const [name, entry] of Object.entries(virtuals)) {
1436
+ if (entry === void 0) continue;
1437
+ const path = join(prefix, name);
1438
+ if (isVirtualContainerValue(entry)) visitVirtuals(entry.sub, path);
1439
+ else forbidden.add(path);
1440
+ }
1441
+ };
1442
+ visitVirtuals(options.virtuals ?? {}, "");
1443
+ const allowed = (path) => ![...forbidden].some((blocked) => within(path, blocked));
1444
+ return {
1445
+ leaves: [...leaves].filter(allowed).sort(),
1446
+ paths: new Set([...leaves, ...containers].filter(allowed))
1447
+ };
1448
+ }
1449
+ function normalizeSelection(select, catalog, label) {
1450
+ if (select !== void 0 && typeof select !== "string") {
1451
+ if (Array.isArray(select)) {
1452
+ if (Array.from(select).some((token) => typeof token !== "string")) fail(label, "select arrays require strings");
1453
+ } else {
1454
+ const projection = record(select, label);
1455
+ for (const [path, value] of Object.entries(projection)) {
1456
+ if (!isSafeInputPath(path, true) || value !== 1 && value !== -1)
1457
+ fail(label, "select objects require safe persisted paths with 1/-1 values");
1458
+ }
1459
+ }
1460
+ }
1461
+ const tokens = normalizeSelect(select);
1462
+ for (const token of tokens) {
1463
+ const path = token.startsWith("-") ? token.slice(1) : token;
1464
+ if (!isSafeInputPath(path, true) || !catalog.paths.has(path))
1465
+ fail(label, `select path "${path}" is not an eligible static persisted target path`);
1466
+ }
1467
+ const positive = tokens.filter((token) => !token.startsWith("-"));
1468
+ const negative = tokens.filter((token) => token.startsWith("-")).map((token) => token.slice(1));
1469
+ const selection = catalog.leaves.filter(
1470
+ (path) => (path === "_id" || positive.length === 0 || positive.some((included) => within(path, included))) && !negative.some((excluded) => within(path, excluded))
1471
+ );
1472
+ return {
1473
+ selection: Object.freeze(selection),
1474
+ fetchSelection: Object.freeze([.../* @__PURE__ */ new Set(["_id", ...selection])].sort()),
1475
+ includeId: selection.includes("_id")
1476
+ };
1477
+ }
1478
+ function captureVirtualPopulateDependencies(runtime, modelName, options) {
1479
+ const model = runtime.getModelInstance(modelName);
1480
+ const targets = /* @__PURE__ */ new Map();
1481
+ const scopes = [];
1482
+ const visit = (virtuals, schema, schemaPrefix, scopePath, unsupportedScope) => {
1483
+ const sourcePrefix = scopePath.filter((_, index) => index % 2 === 0).join(".");
1484
+ const captureList = (value, label) => {
1485
+ if (value === void 0) return Object.freeze([]);
1486
+ if (!Array.isArray(value)) return fail(label, "must be an array");
1487
+ const seen = /* @__PURE__ */ new Set();
1488
+ return Object.freeze(
1489
+ Array.from(value, (entry) => {
1490
+ const dependency = record(entry, label);
1491
+ if (Object.keys(dependency).some((key) => key !== "path" && key !== "select"))
1492
+ fail(label, "accepts only path/select");
1493
+ const path = dependency.path;
1494
+ if (typeof path !== "string" || path.includes(".") || !isSafeInputPath(path, true))
1495
+ return fail(label, "path must be a safe scope-local persisted reference key");
1496
+ if (seen.has(path)) fail(label, `duplicate path "${path}"`);
1497
+ seen.add(path);
1498
+ if (within(join(sourcePrefix, path), options.documentPermissionField ?? "_permissions") || path === schema?.options.versionKey)
1499
+ fail(label, "source permission metadata/version paths are not reference inputs");
1500
+ if (!model || !schema || unsupportedScope)
1501
+ return fail(label, "requires a static embedded, non-discriminator source scope");
1502
+ const type = schema.path(join(schemaPrefix, path));
1503
+ const isArray6 = type?.instance === "Array";
1504
+ const scalar = type && (isArray6 ? elementType(type) : type);
1505
+ const ref = type?.options?.ref ?? scalar?.options?.ref;
1506
+ if (!scalar || scalar.instance !== "ObjectId" || typeof ref !== "string" || !ref || type?.options?.refPath !== void 0 || scalar.options?.refPath !== void 0)
1507
+ return fail(label, `path "${path}" requires a static string ref on an ObjectId scalar or 1D array`);
1508
+ let captured = targets.get(ref);
1509
+ if (!captured) {
1510
+ if (ref !== modelName && !runtime.hasModel(ref))
1511
+ return fail(label, `target "${ref}" must be configured first in this runtime`);
1512
+ const targetModel = runtime.getModelInstance(ref);
1513
+ if (!targetModel || targetModel.db !== model.db || model.db.models[ref] !== targetModel || varyingModel(targetModel) || targetModel.schema.path("_id")?.instance !== "ObjectId")
1514
+ return fail(
1515
+ label,
1516
+ `target "${ref}" requires a registered same-connection non-discriminator ObjectId model bound to the source ref`
1517
+ );
1518
+ const targetOptions = ref === modelName ? options : runtime.getModelOptions(ref);
1519
+ const defaults = runtime.getDefaultModelOptions();
1520
+ const readOption = (key) => resolveNestedOption((path2, fallback) => get2(targetOptions, path2, fallback), key, get2(defaults, key));
1521
+ const documentPermissionField = targetOptions.documentPermissionField ?? defaults.documentPermissionField;
1522
+ const catalog = captureAuthorizationFieldCatalog(targetModel.schema, void 0, documentPermissionField);
1523
+ const configuration = copyAuthorizationValue(
1524
+ {
1525
+ catalog,
1526
+ documentPermissionField,
1527
+ operationAccess: { read: readOption("operationAccess.read") },
1528
+ baseFilter: { read: readOption("baseFilter.read") },
1529
+ overrideFilter: { read: readOption("overrideFilter.read") },
1530
+ hydration: targetOptions.hydration
1531
+ },
1532
+ true
1533
+ );
1534
+ captured = {
1535
+ target: Object.freeze({
1536
+ identity: Object.freeze({}),
1537
+ runtime,
1538
+ modelName: ref,
1539
+ model: targetModel,
1540
+ schema: targetModel.schema,
1541
+ configuration
1542
+ }),
1543
+ catalog: targetSelectionCatalog(targetModel.schema, { ...targetOptions, documentPermissionField })
1544
+ };
1545
+ targets.set(ref, captured);
1546
+ }
1547
+ return Object.freeze({
1548
+ path,
1549
+ sourcePath: join(sourcePrefix, path),
1550
+ cardinality: isArray6 ? "array" : "scalar",
1551
+ target: captured.target,
1552
+ ...normalizeSelection(dependency.select, captured.catalog, label)
1553
+ });
1554
+ })
1555
+ );
1556
+ };
1557
+ for (const [name, raw] of Object.entries(virtuals)) {
1558
+ if (isVirtualContainerValue(raw)) {
1559
+ const path = join(schemaPrefix, name);
1560
+ const type = schema?.path(path);
1561
+ const element = type && elementType(type);
1562
+ const child = type?.schema ?? element?.schema;
1563
+ const nonEmbedded = [type, element].some(
1564
+ (entry2) => entry2 && (entry2.options?.ref !== void 0 || entry2.options?.refPath !== void 0 || entry2.instance === "Map" || entry2.instance === "Mixed")
1565
+ );
1566
+ visit(
1567
+ raw.sub,
1568
+ child ?? schema,
1569
+ child ? "" : path,
1570
+ [...scopePath, name, "sub"],
1571
+ unsupportedScope || nonEmbedded || !!(child && varyingSchema(child))
1572
+ );
1573
+ continue;
1574
+ }
1575
+ if (!isPlainObject11(raw)) continue;
1576
+ const entry = raw;
1577
+ const label = join(sourcePrefix, name);
1578
+ captureList(entry.dependsOnPopulate, label);
1579
+ if (!("get" in entry))
1580
+ for (const access of accesses) {
1581
+ if (isPlainObject11(entry[access]))
1582
+ captureList(entry[access].dependsOnPopulate, `${label}.${access}`);
1583
+ }
1584
+ const byAccess = {};
1585
+ let declared = false;
1586
+ for (const access of accesses) {
1587
+ const descriptor = resolveSnapshotDescriptor(virtuals, name, access);
1588
+ if (descriptor) {
1589
+ byAccess[access] = captureList(descriptor.dependsOnPopulate, `${label}.${access}`);
1590
+ declared ||= descriptor.dependsOnPopulate !== void 0;
1591
+ }
1592
+ }
1593
+ if (declared)
1594
+ scopes.push(
1595
+ Object.freeze({
1596
+ scopePath: Object.freeze([...scopePath]),
1597
+ virtualName: name,
1598
+ byAccess: Object.freeze(byAccess)
1599
+ })
1600
+ );
1601
+ }
1602
+ };
1603
+ visit(options.virtuals ?? {}, model?.schema, "", [], !!model && varyingModel(model));
1604
+ return Object.freeze({ scopes: Object.freeze(scopes) });
1605
+ }
1606
+
1114
1607
  // src/runtime.ts
1115
1608
  var mongooseJsonSchemaInitialized = false;
1116
1609
  var ensureMongooseJsonSchemaInitialized = () => {
@@ -1151,7 +1644,7 @@ var hasModelPermission = (value, modelPermissionPrefix) => {
1151
1644
  return true;
1152
1645
  });
1153
1646
  }
1154
- return isFunction(value);
1647
+ return isFunction2(value);
1155
1648
  };
1156
1649
  var classifyPermissionSchema = (permissionSchema, modelPermissionPrefix) => {
1157
1650
  const schemaKeys = Object.keys(permissionSchema);
@@ -1159,7 +1652,7 @@ var classifyPermissionSchema = (permissionSchema, modelPermissionPrefix) => {
1159
1652
  const modelPermissionKeys = {};
1160
1653
  forEach2(schemaKeys, (schemaKey) => {
1161
1654
  const schemaRule = permissionSchema[schemaKey];
1162
- const ruleEntries = isPlainObject10(schemaRule) ? Object.entries(schemaRule) : FIELD_ACCESS_KEYS.map((accessKey) => [accessKey, schemaRule]);
1655
+ const ruleEntries = isPlainObject12(schemaRule) ? Object.entries(schemaRule) : FIELD_ACCESS_KEYS.map((accessKey) => [accessKey, schemaRule]);
1163
1656
  for (let x = 0; x < ruleEntries.length; x++) {
1164
1657
  const [accessKey, value] = ruleEntries[x];
1165
1658
  if (!isArray4(globalPermissionKeys[accessKey])) {
@@ -1200,13 +1693,13 @@ var VIRTUAL_UNSUPPORTED_ACCESSES = ["delete", "distinct", "count"];
1200
1693
  var VIRTUAL_RESERVED_NAMES = /* @__PURE__ */ new Set(["_id", "__v", "id"]);
1201
1694
  var VIRTUAL_DANGEROUS_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1202
1695
  var VIRTUAL_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
1203
- var isVirtualContainerValue = (value) => isPlainObject10(value) && "sub" in value;
1696
+ var isVirtualContainerValue2 = (value) => isPlainObject12(value) && "sub" in value;
1204
1697
  var virtualFullPath = (prefix, name) => prefix ? `${prefix}.${name}` : name;
1205
1698
  var virtualPathsOverlap = (virtualPath, permissionField) => {
1206
1699
  if (!virtualPath || !permissionField) return false;
1207
1700
  try {
1208
- if (get2(set3({}, virtualPath, true), permissionField) !== void 0) return true;
1209
- if (get2(set3({}, permissionField, true), virtualPath) !== void 0) return true;
1701
+ if (get3(set3({}, virtualPath, true), permissionField) !== void 0) return true;
1702
+ if (get3(set3({}, permissionField, true), virtualPath) !== void 0) return true;
1210
1703
  } catch {
1211
1704
  return false;
1212
1705
  }
@@ -1294,18 +1787,20 @@ var validateVirtualDependsOn = (dependsOn, entryLabel, storedFields, siblingVirt
1294
1787
  return list;
1295
1788
  };
1296
1789
  var validateVirtualLeafValue = (value, entryLabel, storedFields, siblingVirtualNames, sharedDependsOn) => {
1297
- if (isFunction(value)) return;
1298
- if (!isPlainObject10(value)) {
1790
+ if (isFunction2(value)) return;
1791
+ if (!isPlainObject12(value)) {
1299
1792
  throw new Error(`Virtual ${entryLabel}: descriptor must be a getter function or { get, dependsOn }`);
1300
1793
  }
1301
- const record = value;
1302
- if (!isFunction(record.get)) {
1794
+ const record2 = value;
1795
+ if (!isFunction2(record2.get)) {
1303
1796
  throw new Error(`Virtual ${entryLabel}: descriptor.get must be a callable getter function`);
1304
1797
  }
1305
- validateVirtualDependsOn(record.dependsOn ?? sharedDependsOn, entryLabel, storedFields, siblingVirtualNames);
1306
- for (const k of Object.keys(record)) {
1307
- if (k !== "get" && k !== "dependsOn") {
1308
- throw new Error(`Virtual ${entryLabel}: unsupported descriptor key "${k}" (expected only get/dependsOn)`);
1798
+ validateVirtualDependsOn(record2.dependsOn ?? sharedDependsOn, entryLabel, storedFields, siblingVirtualNames);
1799
+ for (const k of Object.keys(record2)) {
1800
+ if (k !== "get" && k !== "dependsOn" && k !== "dependsOnPopulate") {
1801
+ throw new Error(
1802
+ `Virtual ${entryLabel}: unsupported descriptor key "${k}" (expected get/dependsOn/dependsOnPopulate)`
1803
+ );
1309
1804
  }
1310
1805
  }
1311
1806
  };
@@ -1314,7 +1809,7 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1314
1809
  const leafNames = /* @__PURE__ */ new Set();
1315
1810
  for (const [name, raw] of Object.entries(scopeVirtuals)) {
1316
1811
  if (raw === void 0) continue;
1317
- if (isVirtualContainerValue(raw)) continue;
1812
+ if (isVirtualContainerValue2(raw)) continue;
1318
1813
  leafNames.add(name);
1319
1814
  }
1320
1815
  for (const [name, raw] of Object.entries(scopeVirtuals)) {
@@ -1331,15 +1826,15 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1331
1826
  `Virtual ${scopeLabel}${name}: overlaps document permission field "${documentPermissionField}" (equal/ancestor/descendant rejected)`
1332
1827
  );
1333
1828
  }
1334
- if (isVirtualContainerValue(raw)) {
1335
- const record2 = raw;
1336
- const extra = Object.keys(record2).filter((k) => k !== "sub");
1829
+ if (isVirtualContainerValue2(raw)) {
1830
+ const record3 = raw;
1831
+ const extra = Object.keys(record3).filter((k) => k !== "sub");
1337
1832
  if (extra.length > 0) {
1338
1833
  throw new Error(
1339
1834
  `Virtual ${scopeLabel}${name}: scope-container must only hold "sub" (embedded container "${name}" is not a virtual-leaf collision)`
1340
1835
  );
1341
1836
  }
1342
- if (!isPlainObject10(record2.sub)) {
1837
+ if (!isPlainObject12(record3.sub)) {
1343
1838
  throw new Error(`Virtual ${scopeLabel}${name}: scope-container "sub" must be a plain object`);
1344
1839
  }
1345
1840
  continue;
@@ -1349,19 +1844,19 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1349
1844
  `Virtual ${scopeLabel}${name}: collides with a stored path (inspected actual Mongoose schema/child scope; getModelAtt alone is insufficient)`
1350
1845
  );
1351
1846
  }
1352
- if (isFunction(raw)) continue;
1353
- if (!isPlainObject10(raw)) {
1847
+ if (isFunction2(raw)) continue;
1848
+ if (!isPlainObject12(raw)) {
1354
1849
  throw new Error(
1355
1850
  `Virtual ${scopeLabel}${name}: malformed descriptor (expected getter, { get, dependsOn }, or per-access record)`
1356
1851
  );
1357
1852
  }
1358
- const record = raw;
1359
- const hasGet = "get" in record;
1360
- const accessKeys = Object.keys(record).filter((k) => VIRTUAL_SUPPORTED_ACCESSES.includes(k));
1361
- const unsupported2 = Object.keys(record).filter(
1362
- (k) => k !== "get" && k !== "dependsOn" && k !== "sub" && !VIRTUAL_SUPPORTED_ACCESSES.includes(k)
1853
+ const record2 = raw;
1854
+ const hasGet = "get" in record2;
1855
+ const accessKeys = Object.keys(record2).filter((k) => VIRTUAL_SUPPORTED_ACCESSES.includes(k));
1856
+ const unsupported2 = Object.keys(record2).filter(
1857
+ (k) => k !== "get" && k !== "dependsOn" && k !== "dependsOnPopulate" && k !== "sub" && !VIRTUAL_SUPPORTED_ACCESSES.includes(k)
1363
1858
  );
1364
- for (const key of Object.keys(record)) {
1859
+ for (const key of Object.keys(record2)) {
1365
1860
  if (VIRTUAL_UNSUPPORTED_ACCESSES.includes(key)) {
1366
1861
  throw new Error(
1367
1862
  `Virtual ${scopeLabel}${name}: unsupported access "${key}" (supported: default + list/create/read/update; NOT delete/distinct/count)`
@@ -1379,21 +1874,21 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1379
1874
  `Virtual ${scopeLabel}${name}: cannot mix "get" with per-access keys (use either bare { get, dependsOn } or a per-access record)`
1380
1875
  );
1381
1876
  }
1382
- if ("sub" in record) {
1877
+ if ("sub" in record2) {
1383
1878
  throw new Error(`Virtual ${scopeLabel}${name}: cannot mix "get" with "sub" (leaf vs embedded container)`);
1384
1879
  }
1385
1880
  validateVirtualLeafValue(raw, `${scopeLabel}${name}`, storedFields, leafNames, void 0);
1386
1881
  continue;
1387
1882
  }
1388
1883
  if (accessKeys.length > 0) {
1389
- if ("sub" in record) {
1884
+ if ("sub" in record2) {
1390
1885
  throw new Error(`Virtual ${scopeLabel}${name}: cannot mix per-access keys with "sub"`);
1391
1886
  }
1392
- validateVirtualDependsOn(record.dependsOn, `${scopeLabel}${name}`, storedFields, leafNames);
1887
+ validateVirtualDependsOn(record2.dependsOn, `${scopeLabel}${name}`, storedFields, leafNames);
1393
1888
  for (const access of accessKeys) {
1394
- const leaf = record[access];
1889
+ const leaf = record2[access];
1395
1890
  if (leaf === void 0) continue;
1396
- validateVirtualLeafValue(leaf, `${scopeLabel}${name}.${access}`, storedFields, leafNames, record.dependsOn);
1891
+ validateVirtualLeafValue(leaf, `${scopeLabel}${name}.${access}`, storedFields, leafNames, record2.dependsOn);
1397
1892
  }
1398
1893
  continue;
1399
1894
  }
@@ -1404,7 +1899,7 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1404
1899
  };
1405
1900
  var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName) => {
1406
1901
  if (virtuals === void 0 || virtuals === null) return;
1407
- if (!isPlainObject10(virtuals)) {
1902
+ if (!isPlainObject12(virtuals)) {
1408
1903
  throw new Error(`Virtuals for model "${modelName}": virtuals must be a plain object mapping names to descriptors`);
1409
1904
  }
1410
1905
  const rootSchema = model?.schema ?? null;
@@ -1414,13 +1909,14 @@ var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName)
1414
1909
  { scopeVirtuals: rootVirtuals, storedFields: rootStored, scopePrefix: "", scopeLabel: "" },
1415
1910
  documentPermissionField
1416
1911
  );
1417
- const visitContainers = (currentVirtuals, currentSchema, currentPrefix) => {
1912
+ const visitContainers = (currentVirtuals, currentSchema, currentPrefix, schemaPrefix = "") => {
1418
1913
  for (const [containerName, raw] of Object.entries(currentVirtuals)) {
1419
- if (raw === void 0 || !isVirtualContainerValue(raw)) continue;
1420
- const record = raw;
1421
- const childSchema = getChildSchemaForField(currentSchema, containerName);
1422
- const nestedFields = getNestedChildFields(currentSchema, containerName);
1423
- const currentStored = collectStoredTopLevel(currentSchema);
1914
+ if (raw === void 0 || !isVirtualContainerValue2(raw)) continue;
1915
+ const record2 = raw;
1916
+ const schemaPath = virtualFullPath(schemaPrefix, containerName);
1917
+ const childSchema = getChildSchemaForField(currentSchema, schemaPath);
1918
+ const nestedFields = getNestedChildFields(currentSchema, schemaPath);
1919
+ const currentStored = schemaPrefix ? getNestedChildFields(currentSchema, schemaPrefix) : collectStoredTopLevel(currentSchema);
1424
1920
  if (!currentStored.has(containerName)) {
1425
1921
  throw new Error(
1426
1922
  `Virtual ${containerName}: scope-container "${containerName}" is not a persisted field in its scope`
@@ -1434,7 +1930,7 @@ var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName)
1434
1930
  }
1435
1931
  const subStored = childSchema ? collectStoredTopLevel(childSchema) : nestedFields;
1436
1932
  const subPrefix = currentPrefix ? `${currentPrefix}.${containerName}` : containerName;
1437
- const subVirtuals = record.sub ?? {};
1933
+ const subVirtuals = record2.sub ?? {};
1438
1934
  validateVirtualsScope(
1439
1935
  {
1440
1936
  scopeVirtuals: subVirtuals,
@@ -1444,37 +1940,11 @@ var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName)
1444
1940
  },
1445
1941
  documentPermissionField
1446
1942
  );
1447
- const nextSchema = childSchema;
1448
- visitContainers(subVirtuals, nextSchema, subPrefix);
1943
+ visitContainers(subVirtuals, childSchema ?? currentSchema, subPrefix, childSchema ? "" : schemaPath);
1449
1944
  }
1450
1945
  };
1451
1946
  visitContainers(rootVirtuals, rootSchema, "");
1452
1947
  };
1453
- var getScopeVirtualsObject = (rootVirtuals, scopePath) => {
1454
- if (!rootVirtuals || !isPlainObject10(rootVirtuals)) return null;
1455
- if (!scopePath || scopePath.length === 0) return rootVirtuals;
1456
- let current = rootVirtuals;
1457
- for (let i = 0; i < scopePath.length; i += 2) {
1458
- const field = scopePath[i];
1459
- const subMarker = scopePath[i + 1];
1460
- if (!field || subMarker !== "sub") return null;
1461
- if (!isPlainObject10(current)) return null;
1462
- const container = current[field];
1463
- if (!isVirtualContainerValue(container)) return null;
1464
- current = container.sub;
1465
- if (!isPlainObject10(current)) return null;
1466
- }
1467
- return current;
1468
- };
1469
- var toVirtualDescriptor = (leaf, sharedDependsOn) => {
1470
- if (isFunction(leaf))
1471
- return { get: leaf, dependsOn: sharedDependsOn ?? [] };
1472
- if (!isPlainObject10(leaf)) return null;
1473
- const record = leaf;
1474
- if (!isFunction(record.get)) return null;
1475
- const dependsOn = record.dependsOn ?? sharedDependsOn ?? [];
1476
- return { get: record.get, dependsOn };
1477
- };
1478
1948
  var AccessRuntime = class {
1479
1949
  constructor(options = {}) {
1480
1950
  this.globalOptions = new OptionsManager(
@@ -1736,13 +2206,14 @@ var AccessRuntime = class {
1736
2206
  setModelOptions(modelName, options) {
1737
2207
  assertPublicDefaults(options.defaults);
1738
2208
  if ("hydration" in options) options = { ...options, hydration: normalizeHydration(options.hydration) };
1739
- const manager = this.getOrCreateModelOptions(modelName);
1740
2209
  const defaultOptions = this.getDefaultModelOptions();
1741
- const currentModelOptions = manager.fetch();
2210
+ const currentModelOptions = this.modelOptions[modelName]?.fetch() ?? { ...defaultModelOptions, modelName };
1742
2211
  const merged = { ...defaultOptions, ...currentModelOptions, ...options };
1743
2212
  const model = this.getModelInstance(modelName);
1744
2213
  const permissionField = merged.documentPermissionField ?? currentModelOptions.documentPermissionField ?? this.getDefaultModelOption("documentPermissionField") ?? "_permissions";
1745
2214
  validateVirtualsTree(model, merged.virtuals, permissionField, modelName);
2215
+ captureVirtualPopulateDependencies(this, modelName, merged);
2216
+ const manager = this.getOrCreateModelOptions(modelName);
1746
2217
  manager.assign({ ...defaultOptions, ...currentModelOptions, ...options });
1747
2218
  }
1748
2219
  setModelOption(modelName, key, value) {
@@ -1758,11 +2229,11 @@ var AccessRuntime = class {
1758
2229
  set3(proposed, keyStr.slice("defaults.".length), value);
1759
2230
  assertPublicDefaults(proposed);
1760
2231
  }
1761
- const manager = this.getOrCreateModelOptions(modelName);
2232
+ const manager = this.modelOptions[modelName];
1762
2233
  if (keyStr === "virtuals" || keyStr.startsWith("virtuals.") || keyStr === "documentPermissionField") {
1763
2234
  const model = this.getModelInstance(modelName);
1764
- const currentVirtuals = manager.get("virtuals");
1765
- const currentPermissionField = manager.get("documentPermissionField") ?? this.getDefaultModelOption("documentPermissionField") ?? "_permissions";
2235
+ const currentVirtuals = manager?.get("virtuals");
2236
+ const currentPermissionField = manager?.get("documentPermissionField") ?? this.getDefaultModelOption("documentPermissionField") ?? "_permissions";
1766
2237
  let proposedVirtuals = currentVirtuals;
1767
2238
  let proposedPermissionField = currentPermissionField;
1768
2239
  if (keyStr === "documentPermissionField") {
@@ -1776,10 +2247,10 @@ var AccessRuntime = class {
1776
2247
  const segments = nestedPath.split(".");
1777
2248
  let cursor = cloned;
1778
2249
  for (let i = 0; i < segments.length - 1; i++) {
1779
- if (!isPlainObject10(cursor)) break;
2250
+ if (!isPlainObject12(cursor)) break;
1780
2251
  cursor = cursor[segments[i]];
1781
2252
  }
1782
- if (isPlainObject10(cursor)) {
2253
+ if (isPlainObject12(cursor)) {
1783
2254
  delete cursor[segments[segments.length - 1]];
1784
2255
  }
1785
2256
  proposedVirtuals = cloned;
@@ -1789,8 +2260,14 @@ var AccessRuntime = class {
1789
2260
  }
1790
2261
  }
1791
2262
  validateVirtualsTree(model, proposedVirtuals, proposedPermissionField, modelName);
2263
+ captureVirtualPopulateDependencies(this, modelName, {
2264
+ ...this.getDefaultModelOptions(),
2265
+ ...manager?.fetch(),
2266
+ virtuals: proposedVirtuals,
2267
+ documentPermissionField: proposedPermissionField
2268
+ });
1792
2269
  }
1793
- manager.set(key, value);
2270
+ this.getOrCreateModelOptions(modelName).set(key, value);
1794
2271
  }
1795
2272
  /** Frozen stored snapshot, not fully resolved per-path defaults. Read again for current configuration. */
1796
2273
  getModelOptions(modelName) {
@@ -1829,7 +2306,7 @@ var AccessRuntime = class {
1829
2306
  const rootVirtuals = manager.get("virtuals");
1830
2307
  const scopeVirtuals = getScopeVirtualsObject(rootVirtuals, scopePath);
1831
2308
  if (!scopeVirtuals) return [];
1832
- return Object.entries(scopeVirtuals).filter(([, v]) => v !== void 0 && !isVirtualContainerValue(v)).map(([k]) => k);
2309
+ return Object.entries(scopeVirtuals).filter(([, v]) => v !== void 0 && !isVirtualContainerValue2(v)).map(([k]) => k);
1833
2310
  }
1834
2311
  /**
1835
2312
  * Whether a field is a registered virtual leaf in a scope (VIRT-01).
@@ -1843,7 +2320,7 @@ var AccessRuntime = class {
1843
2320
  const scopeVirtuals = getScopeVirtualsObject(rootVirtuals, scopePath);
1844
2321
  if (!scopeVirtuals || !(fieldName in scopeVirtuals)) return false;
1845
2322
  const entry = scopeVirtuals[fieldName];
1846
- if (entry === void 0 || isVirtualContainerValue(entry)) return false;
2323
+ if (entry === void 0 || isVirtualContainerValue2(entry)) return false;
1847
2324
  return true;
1848
2325
  }
1849
2326
  /**
@@ -1853,34 +2330,14 @@ var AccessRuntime = class {
1853
2330
  * interpreted as a getter descriptor when the access is absent: an
1854
2331
  * inapplicable registered name stays virtual (excluded from persisted
1855
2332
  * projections, write admission, and DB sort/filter/distinct).
1856
- * Shared record-level `dependsOn` is inherited when a leaf lacks its own.
2333
+ * Shared `dependsOn` and `dependsOnPopulate` lists inherit independently;
2334
+ * each defined leaf list replaces its shared list wholesale.
1857
2335
  */
1858
2336
  resolveVirtualDescriptor(modelName, virtualName, access, scopePath) {
1859
2337
  const manager = this.getOrCreateModelOptions(modelName);
1860
2338
  const rootVirtuals = manager.get("virtuals");
1861
2339
  const scopeVirtuals = getScopeVirtualsObject(rootVirtuals, scopePath);
1862
- if (!scopeVirtuals || !(virtualName in scopeVirtuals)) return void 0;
1863
- const entry = scopeVirtuals[virtualName];
1864
- if (entry === void 0 || isVirtualContainerValue(entry)) return void 0;
1865
- if (isFunction(entry)) return { get: entry, dependsOn: [] };
1866
- if (!isPlainObject10(entry)) return void 0;
1867
- const record = entry;
1868
- if ("get" in record) {
1869
- const descriptor = toVirtualDescriptor(entry, void 0);
1870
- return descriptor ?? void 0;
1871
- }
1872
- const sharedDependsOn = record.dependsOn;
1873
- const exact = record[access];
1874
- if (exact !== void 0) {
1875
- const descriptor = toVirtualDescriptor(exact, sharedDependsOn);
1876
- if (descriptor) return descriptor;
1877
- }
1878
- const fallback = record.default;
1879
- if (fallback !== void 0) {
1880
- const descriptor = toVirtualDescriptor(fallback, sharedDependsOn);
1881
- if (descriptor) return descriptor;
1882
- }
1883
- return void 0;
2340
+ return resolveSnapshotDescriptor(scopeVirtuals, virtualName, access);
1884
2341
  }
1885
2342
  getModelNames() {
1886
2343
  return Object.keys(this.modelOptions);
@@ -1963,16 +2420,16 @@ var AccessRuntime = class {
1963
2420
  }
1964
2421
  getModelRef(modelName, refPath) {
1965
2422
  this.ensureModelMeta(modelName);
1966
- const value = get2(this.modelRefs, `${modelName}.${refPath}`, null);
2423
+ const value = get3(this.modelRefs, `${modelName}.${refPath}`, null);
1967
2424
  return isString3(value) ? value : null;
1968
2425
  }
1969
2426
  getModelSub(modelName) {
1970
2427
  this.ensureModelMeta(modelName);
1971
- return get2(this.modelSubs, modelName, []);
2428
+ return get3(this.modelSubs, modelName, []);
1972
2429
  }
1973
2430
  getModelAtt(modelName) {
1974
2431
  this.ensureModelMeta(modelName);
1975
- return get2(this.modelAtts, modelName, []);
2432
+ return get3(this.modelAtts, modelName, []);
1976
2433
  }
1977
2434
  };
1978
2435
  var defaultRuntime = new AccessRuntime({ allowGlobalModelLookup: true });
@@ -1992,7 +2449,7 @@ var getGlobalOption = (key, defaultValue) => {
1992
2449
 
1993
2450
  // src/validation/common.ts
1994
2451
  import { z as z2 } from "zod";
1995
- import { isPlainObject as isPlainObject12 } from "@web-ts-toolkit/utils";
2452
+ import { isPlainObject as isPlainObject14 } from "@web-ts-toolkit/utils";
1996
2453
 
1997
2454
  // src/validation/public-admission-schema.ts
1998
2455
  var admissions = /* @__PURE__ */ new WeakMap();
@@ -2014,7 +2471,7 @@ function assertTransformedPublicAdmission(schema, value) {
2014
2471
  }
2015
2472
 
2016
2473
  // src/correlated-includes.ts
2017
- import { cloneDeep, isArray as isArray5, isPlainObject as isPlainObject11 } from "@web-ts-toolkit/utils";
2474
+ import { cloneDeep, isArray as isArray5, isPlainObject as isPlainObject13 } from "@web-ts-toolkit/utils";
2018
2475
  var PARENT_MARKER_KEY = "$parent";
2019
2476
  var ESCAPE_WRAPPER_KEY = "$escape";
2020
2477
  var SUBQUERY_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["$$sq", "$$date"]);
@@ -2025,10 +2482,10 @@ var FIELD_SEGMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
2025
2482
  var ARRAY_INDEX_SEGMENT_PATTERN = /^(0|[1-9]\d*)$/;
2026
2483
  var ownKeys = (value) => Object.keys(value);
2027
2484
  function isParentRefMarker(value) {
2028
- return isPlainObject11(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === PARENT_MARKER_KEY;
2485
+ return isPlainObject13(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === PARENT_MARKER_KEY;
2029
2486
  }
2030
2487
  function isEscapedParentRef(value) {
2031
- return isPlainObject11(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === ESCAPE_WRAPPER_KEY;
2488
+ return isPlainObject13(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === ESCAPE_WRAPPER_KEY;
2032
2489
  }
2033
2490
  function hasMarkerKey(value) {
2034
2491
  return ownKeys(value).includes(PARENT_MARKER_KEY) || ownKeys(value).includes(ESCAPE_WRAPPER_KEY);
@@ -2052,7 +2509,7 @@ function containsParentMarkerShape(value) {
2052
2509
  seen.add(current);
2053
2510
  return current.some(visit);
2054
2511
  }
2055
- if (isPlainObject11(current)) {
2512
+ if (isPlainObject13(current)) {
2056
2513
  if (seen.has(current)) return false;
2057
2514
  seen.add(current);
2058
2515
  if (hasMarkerKey(current)) return true;
@@ -2082,7 +2539,7 @@ function scanTemplateValueNode(value, path, issues) {
2082
2539
  }
2083
2540
  return;
2084
2541
  }
2085
- if (isPlainObject11(value)) {
2542
+ if (isPlainObject13(value)) {
2086
2543
  if (hasMarkerKey(value)) {
2087
2544
  issues.push({
2088
2545
  message: 'Malformed $parent reference: objects carrying $parent must be exactly { "$parent": "<field>" }',
@@ -2147,7 +2604,7 @@ function scanTemplateObjectEntries(obj, basePath, issues) {
2147
2604
  });
2148
2605
  return;
2149
2606
  }
2150
- if (isPlainObject11(clause)) {
2607
+ if (isPlainObject13(clause)) {
2151
2608
  scanTemplateObjectEntries(clause, clausePath, issues);
2152
2609
  }
2153
2610
  });
@@ -2158,7 +2615,7 @@ function scanTemplateObjectEntries(obj, basePath, issues) {
2158
2615
  }
2159
2616
  function validateCorrelatedFilterTemplate(filter) {
2160
2617
  const issues = [];
2161
- if (!isPlainObject11(filter)) {
2618
+ if (!isPlainObject13(filter)) {
2162
2619
  issues.push({ message: "Correlated include filter must be an object", path: [] });
2163
2620
  return issues;
2164
2621
  }
@@ -2286,7 +2743,7 @@ var correlatedFilterSchema = z2.record(z2.string(), z2.unknown()).superRefine((f
2286
2743
  }
2287
2744
  });
2288
2745
  var correlatedEmptyOptionsSchema = z2.unknown().refine(
2289
- (value) => value === void 0 || isPlainObject12(value) && Object.keys(value).length === 0,
2746
+ (value) => value === void 0 || isPlainObject14(value) && Object.keys(value).length === 0,
2290
2747
  "Correlated include options must be absent or empty"
2291
2748
  ).optional();
2292
2749
  var correlatedPathSchema = z2.string().min(1).superRefine((path, ctx) => {
@@ -2323,7 +2780,7 @@ var correlatedListArgsSchema = z2.object({
2323
2780
  include: z2.lazy(() => correlatedNestedIncludeSchema).optional()
2324
2781
  }).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
2325
2782
  var correlatedCountArgsSchema = z2.unknown().refine(
2326
- (value) => value === void 0 || isPlainObject12(value) && Object.keys(value).length === 0,
2783
+ (value) => value === void 0 || isPlainObject14(value) && Object.keys(value).length === 0,
2327
2784
  "Count includes accept no args"
2328
2785
  ).optional();
2329
2786
  var correlatedReadByIdSchema = z2.object({
@@ -2376,7 +2833,7 @@ var correlatedNestedIncludeSchema = z2.union([includeItemSchema, z2.array(includ
2376
2833
  var rejectDuplicateCorrelatedPaths = (items, ctx) => {
2377
2834
  const seen = /* @__PURE__ */ new Set();
2378
2835
  items.forEach((item, index) => {
2379
- if (isPlainObject12(item) && item.mode === "correlated" && typeof item.path === "string") {
2836
+ if (isPlainObject14(item) && item.mode === "correlated" && typeof item.path === "string") {
2380
2837
  if (seen.has(item.path)) {
2381
2838
  ctx.addIssue({
2382
2839
  code: z2.ZodIssueCode.custom,
@@ -2414,7 +2871,7 @@ function rejectKeys(body, ctx, keys2) {
2414
2871
  }
2415
2872
 
2416
2873
  // src/validation/parsers.ts
2417
- var clientErrors = JsonRouter2.clientErrors;
2874
+ var clientErrors = JsonRouter3.clientErrors;
2418
2875
  function parsePathParam(value, parameter) {
2419
2876
  const result = stringOrStringArray.safeParse(value);
2420
2877
  if (!result.success) {
@@ -2846,8 +3303,8 @@ function isArkTypeErrors(value) {
2846
3303
  if (typeof value !== "object" || value === null) {
2847
3304
  return false;
2848
3305
  }
2849
- const record = value;
2850
- return record.arkKind === "errors" || record[" arkKind"] === "errors";
3306
+ const record2 = value;
3307
+ return record2.arkKind === "errors" || record2[" arkKind"] === "errors";
2851
3308
  }
2852
3309
  function isThenable(value) {
2853
3310
  return (typeof value === "object" || typeof value === "function") && value !== null && "then" in value && typeof value.then === "function";