@web-ts-toolkit/access-router 0.54.0 → 0.56.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,484 @@ 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, globalOptions = runtime.getGlobalOptions()) {
1479
+ const requireReadOperationAccessForPopulateDependencies = globalOptions.requireReadOperationAccessForPopulateDependencies !== false;
1480
+ const model = runtime.getModelInstance(modelName);
1481
+ const targets = /* @__PURE__ */ new Map();
1482
+ const scopes = [];
1483
+ const visit = (virtuals, schema, schemaPrefix, scopePath, unsupportedScope) => {
1484
+ const sourcePrefix = scopePath.filter((_, index) => index % 2 === 0).join(".");
1485
+ const captureList = (value, label) => {
1486
+ if (value === void 0) return Object.freeze([]);
1487
+ if (!Array.isArray(value)) return fail(label, "must be an array");
1488
+ const seen = /* @__PURE__ */ new Set();
1489
+ return Object.freeze(
1490
+ Array.from(value, (entry) => {
1491
+ const dependency = record(entry, label);
1492
+ if (Object.keys(dependency).some((key) => key !== "path" && key !== "select"))
1493
+ fail(label, "accepts only path/select");
1494
+ const path = dependency.path;
1495
+ if (typeof path !== "string" || path.includes(".") || !isSafeInputPath(path, true))
1496
+ return fail(label, "path must be a safe scope-local persisted reference key");
1497
+ if (seen.has(path)) fail(label, `duplicate path "${path}"`);
1498
+ seen.add(path);
1499
+ if (within(join(sourcePrefix, path), options.documentPermissionField ?? "_permissions") || path === schema?.options.versionKey)
1500
+ fail(label, "source permission metadata/version paths are not reference inputs");
1501
+ if (!model || !schema || unsupportedScope)
1502
+ return fail(label, "requires a static embedded, non-discriminator source scope");
1503
+ const type = schema.path(join(schemaPrefix, path));
1504
+ const isArray6 = type?.instance === "Array";
1505
+ const scalar = type && (isArray6 ? elementType(type) : type);
1506
+ const ref = type?.options?.ref ?? scalar?.options?.ref;
1507
+ if (!scalar || scalar.instance !== "ObjectId" || typeof ref !== "string" || !ref || type?.options?.refPath !== void 0 || scalar.options?.refPath !== void 0)
1508
+ return fail(label, `path "${path}" requires a static string ref on an ObjectId scalar or 1D array`);
1509
+ let captured = targets.get(ref);
1510
+ if (!captured) {
1511
+ if (ref !== modelName && !runtime.hasModel(ref))
1512
+ return fail(label, `target "${ref}" must be configured first in this runtime`);
1513
+ const targetModel = runtime.getModelInstance(ref);
1514
+ if (!targetModel || targetModel.db !== model.db || model.db.models[ref] !== targetModel || varyingModel(targetModel) || targetModel.schema.path("_id")?.instance !== "ObjectId")
1515
+ return fail(
1516
+ label,
1517
+ `target "${ref}" requires a registered same-connection non-discriminator ObjectId model bound to the source ref`
1518
+ );
1519
+ const targetOptions = ref === modelName ? options : runtime.getModelOptions(ref);
1520
+ const defaults = runtime.getDefaultModelOptions();
1521
+ const readOption = (key) => resolveNestedOption((path2, fallback) => get2(targetOptions, path2, fallback), key, get2(defaults, key));
1522
+ const documentPermissionField = targetOptions.documentPermissionField ?? defaults.documentPermissionField;
1523
+ const catalog = captureAuthorizationFieldCatalog(targetModel.schema, void 0, documentPermissionField);
1524
+ const configuration = copyAuthorizationValue(
1525
+ {
1526
+ catalog,
1527
+ documentPermissionField,
1528
+ requireReadOperationAccessForPopulateDependencies,
1529
+ operationAccess: { read: readOption("operationAccess.read") },
1530
+ baseFilter: { read: readOption("baseFilter.read") },
1531
+ overrideFilter: { read: readOption("overrideFilter.read") },
1532
+ hydration: targetOptions.hydration
1533
+ },
1534
+ true
1535
+ );
1536
+ captured = {
1537
+ target: Object.freeze({
1538
+ identity: Object.freeze({}),
1539
+ runtime,
1540
+ modelName: ref,
1541
+ model: targetModel,
1542
+ schema: targetModel.schema,
1543
+ configuration
1544
+ }),
1545
+ catalog: targetSelectionCatalog(targetModel.schema, { ...targetOptions, documentPermissionField })
1546
+ };
1547
+ targets.set(ref, captured);
1548
+ }
1549
+ return Object.freeze({
1550
+ path,
1551
+ sourcePath: join(sourcePrefix, path),
1552
+ cardinality: isArray6 ? "array" : "scalar",
1553
+ target: captured.target,
1554
+ ...normalizeSelection(dependency.select, captured.catalog, label)
1555
+ });
1556
+ })
1557
+ );
1558
+ };
1559
+ for (const [name, raw] of Object.entries(virtuals)) {
1560
+ if (isVirtualContainerValue(raw)) {
1561
+ const path = join(schemaPrefix, name);
1562
+ const type = schema?.path(path);
1563
+ const element = type && elementType(type);
1564
+ const child = type?.schema ?? element?.schema;
1565
+ const nonEmbedded = [type, element].some(
1566
+ (entry2) => entry2 && (entry2.options?.ref !== void 0 || entry2.options?.refPath !== void 0 || entry2.instance === "Map" || entry2.instance === "Mixed")
1567
+ );
1568
+ visit(
1569
+ raw.sub,
1570
+ child ?? schema,
1571
+ child ? "" : path,
1572
+ [...scopePath, name, "sub"],
1573
+ unsupportedScope || nonEmbedded || !!(child && varyingSchema(child))
1574
+ );
1575
+ continue;
1576
+ }
1577
+ if (!isPlainObject11(raw)) continue;
1578
+ const entry = raw;
1579
+ const label = join(sourcePrefix, name);
1580
+ captureList(entry.dependsOnPopulate, label);
1581
+ if (!("get" in entry))
1582
+ for (const access of accesses) {
1583
+ if (isPlainObject11(entry[access]))
1584
+ captureList(entry[access].dependsOnPopulate, `${label}.${access}`);
1585
+ }
1586
+ const byAccess = {};
1587
+ let declared = false;
1588
+ for (const access of accesses) {
1589
+ const descriptor = resolveSnapshotDescriptor(virtuals, name, access);
1590
+ if (descriptor) {
1591
+ byAccess[access] = captureList(descriptor.dependsOnPopulate, `${label}.${access}`);
1592
+ declared ||= descriptor.dependsOnPopulate !== void 0;
1593
+ }
1594
+ }
1595
+ if (declared)
1596
+ scopes.push(
1597
+ Object.freeze({
1598
+ scopePath: Object.freeze([...scopePath]),
1599
+ virtualName: name,
1600
+ byAccess: Object.freeze(byAccess)
1601
+ })
1602
+ );
1603
+ }
1604
+ };
1605
+ visit(options.virtuals ?? {}, model?.schema, "", [], !!model && varyingModel(model));
1606
+ return Object.freeze({ scopes: Object.freeze(scopes) });
1607
+ }
1608
+
1114
1609
  // src/runtime.ts
1115
1610
  var mongooseJsonSchemaInitialized = false;
1116
1611
  var ensureMongooseJsonSchemaInitialized = () => {
@@ -1151,7 +1646,7 @@ var hasModelPermission = (value, modelPermissionPrefix) => {
1151
1646
  return true;
1152
1647
  });
1153
1648
  }
1154
- return isFunction(value);
1649
+ return isFunction2(value);
1155
1650
  };
1156
1651
  var classifyPermissionSchema = (permissionSchema, modelPermissionPrefix) => {
1157
1652
  const schemaKeys = Object.keys(permissionSchema);
@@ -1159,7 +1654,7 @@ var classifyPermissionSchema = (permissionSchema, modelPermissionPrefix) => {
1159
1654
  const modelPermissionKeys = {};
1160
1655
  forEach2(schemaKeys, (schemaKey) => {
1161
1656
  const schemaRule = permissionSchema[schemaKey];
1162
- const ruleEntries = isPlainObject10(schemaRule) ? Object.entries(schemaRule) : FIELD_ACCESS_KEYS.map((accessKey) => [accessKey, schemaRule]);
1657
+ const ruleEntries = isPlainObject12(schemaRule) ? Object.entries(schemaRule) : FIELD_ACCESS_KEYS.map((accessKey) => [accessKey, schemaRule]);
1163
1658
  for (let x = 0; x < ruleEntries.length; x++) {
1164
1659
  const [accessKey, value] = ruleEntries[x];
1165
1660
  if (!isArray4(globalPermissionKeys[accessKey])) {
@@ -1200,13 +1695,13 @@ var VIRTUAL_UNSUPPORTED_ACCESSES = ["delete", "distinct", "count"];
1200
1695
  var VIRTUAL_RESERVED_NAMES = /* @__PURE__ */ new Set(["_id", "__v", "id"]);
1201
1696
  var VIRTUAL_DANGEROUS_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1202
1697
  var VIRTUAL_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
1203
- var isVirtualContainerValue = (value) => isPlainObject10(value) && "sub" in value;
1698
+ var isVirtualContainerValue2 = (value) => isPlainObject12(value) && "sub" in value;
1204
1699
  var virtualFullPath = (prefix, name) => prefix ? `${prefix}.${name}` : name;
1205
1700
  var virtualPathsOverlap = (virtualPath, permissionField) => {
1206
1701
  if (!virtualPath || !permissionField) return false;
1207
1702
  try {
1208
- if (get2(set3({}, virtualPath, true), permissionField) !== void 0) return true;
1209
- if (get2(set3({}, permissionField, true), virtualPath) !== void 0) return true;
1703
+ if (get3(set3({}, virtualPath, true), permissionField) !== void 0) return true;
1704
+ if (get3(set3({}, permissionField, true), virtualPath) !== void 0) return true;
1210
1705
  } catch {
1211
1706
  return false;
1212
1707
  }
@@ -1294,18 +1789,20 @@ var validateVirtualDependsOn = (dependsOn, entryLabel, storedFields, siblingVirt
1294
1789
  return list;
1295
1790
  };
1296
1791
  var validateVirtualLeafValue = (value, entryLabel, storedFields, siblingVirtualNames, sharedDependsOn) => {
1297
- if (isFunction(value)) return;
1298
- if (!isPlainObject10(value)) {
1792
+ if (isFunction2(value)) return;
1793
+ if (!isPlainObject12(value)) {
1299
1794
  throw new Error(`Virtual ${entryLabel}: descriptor must be a getter function or { get, dependsOn }`);
1300
1795
  }
1301
- const record = value;
1302
- if (!isFunction(record.get)) {
1796
+ const record2 = value;
1797
+ if (!isFunction2(record2.get)) {
1303
1798
  throw new Error(`Virtual ${entryLabel}: descriptor.get must be a callable getter function`);
1304
1799
  }
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)`);
1800
+ validateVirtualDependsOn(record2.dependsOn ?? sharedDependsOn, entryLabel, storedFields, siblingVirtualNames);
1801
+ for (const k of Object.keys(record2)) {
1802
+ if (k !== "get" && k !== "dependsOn" && k !== "dependsOnPopulate") {
1803
+ throw new Error(
1804
+ `Virtual ${entryLabel}: unsupported descriptor key "${k}" (expected get/dependsOn/dependsOnPopulate)`
1805
+ );
1309
1806
  }
1310
1807
  }
1311
1808
  };
@@ -1314,7 +1811,7 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1314
1811
  const leafNames = /* @__PURE__ */ new Set();
1315
1812
  for (const [name, raw] of Object.entries(scopeVirtuals)) {
1316
1813
  if (raw === void 0) continue;
1317
- if (isVirtualContainerValue(raw)) continue;
1814
+ if (isVirtualContainerValue2(raw)) continue;
1318
1815
  leafNames.add(name);
1319
1816
  }
1320
1817
  for (const [name, raw] of Object.entries(scopeVirtuals)) {
@@ -1331,15 +1828,15 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1331
1828
  `Virtual ${scopeLabel}${name}: overlaps document permission field "${documentPermissionField}" (equal/ancestor/descendant rejected)`
1332
1829
  );
1333
1830
  }
1334
- if (isVirtualContainerValue(raw)) {
1335
- const record2 = raw;
1336
- const extra = Object.keys(record2).filter((k) => k !== "sub");
1831
+ if (isVirtualContainerValue2(raw)) {
1832
+ const record3 = raw;
1833
+ const extra = Object.keys(record3).filter((k) => k !== "sub");
1337
1834
  if (extra.length > 0) {
1338
1835
  throw new Error(
1339
1836
  `Virtual ${scopeLabel}${name}: scope-container must only hold "sub" (embedded container "${name}" is not a virtual-leaf collision)`
1340
1837
  );
1341
1838
  }
1342
- if (!isPlainObject10(record2.sub)) {
1839
+ if (!isPlainObject12(record3.sub)) {
1343
1840
  throw new Error(`Virtual ${scopeLabel}${name}: scope-container "sub" must be a plain object`);
1344
1841
  }
1345
1842
  continue;
@@ -1349,19 +1846,19 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1349
1846
  `Virtual ${scopeLabel}${name}: collides with a stored path (inspected actual Mongoose schema/child scope; getModelAtt alone is insufficient)`
1350
1847
  );
1351
1848
  }
1352
- if (isFunction(raw)) continue;
1353
- if (!isPlainObject10(raw)) {
1849
+ if (isFunction2(raw)) continue;
1850
+ if (!isPlainObject12(raw)) {
1354
1851
  throw new Error(
1355
1852
  `Virtual ${scopeLabel}${name}: malformed descriptor (expected getter, { get, dependsOn }, or per-access record)`
1356
1853
  );
1357
1854
  }
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)
1855
+ const record2 = raw;
1856
+ const hasGet = "get" in record2;
1857
+ const accessKeys = Object.keys(record2).filter((k) => VIRTUAL_SUPPORTED_ACCESSES.includes(k));
1858
+ const unsupported2 = Object.keys(record2).filter(
1859
+ (k) => k !== "get" && k !== "dependsOn" && k !== "dependsOnPopulate" && k !== "sub" && !VIRTUAL_SUPPORTED_ACCESSES.includes(k)
1363
1860
  );
1364
- for (const key of Object.keys(record)) {
1861
+ for (const key of Object.keys(record2)) {
1365
1862
  if (VIRTUAL_UNSUPPORTED_ACCESSES.includes(key)) {
1366
1863
  throw new Error(
1367
1864
  `Virtual ${scopeLabel}${name}: unsupported access "${key}" (supported: default + list/create/read/update; NOT delete/distinct/count)`
@@ -1379,21 +1876,21 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1379
1876
  `Virtual ${scopeLabel}${name}: cannot mix "get" with per-access keys (use either bare { get, dependsOn } or a per-access record)`
1380
1877
  );
1381
1878
  }
1382
- if ("sub" in record) {
1879
+ if ("sub" in record2) {
1383
1880
  throw new Error(`Virtual ${scopeLabel}${name}: cannot mix "get" with "sub" (leaf vs embedded container)`);
1384
1881
  }
1385
1882
  validateVirtualLeafValue(raw, `${scopeLabel}${name}`, storedFields, leafNames, void 0);
1386
1883
  continue;
1387
1884
  }
1388
1885
  if (accessKeys.length > 0) {
1389
- if ("sub" in record) {
1886
+ if ("sub" in record2) {
1390
1887
  throw new Error(`Virtual ${scopeLabel}${name}: cannot mix per-access keys with "sub"`);
1391
1888
  }
1392
- validateVirtualDependsOn(record.dependsOn, `${scopeLabel}${name}`, storedFields, leafNames);
1889
+ validateVirtualDependsOn(record2.dependsOn, `${scopeLabel}${name}`, storedFields, leafNames);
1393
1890
  for (const access of accessKeys) {
1394
- const leaf = record[access];
1891
+ const leaf = record2[access];
1395
1892
  if (leaf === void 0) continue;
1396
- validateVirtualLeafValue(leaf, `${scopeLabel}${name}.${access}`, storedFields, leafNames, record.dependsOn);
1893
+ validateVirtualLeafValue(leaf, `${scopeLabel}${name}.${access}`, storedFields, leafNames, record2.dependsOn);
1397
1894
  }
1398
1895
  continue;
1399
1896
  }
@@ -1404,7 +1901,7 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1404
1901
  };
1405
1902
  var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName) => {
1406
1903
  if (virtuals === void 0 || virtuals === null) return;
1407
- if (!isPlainObject10(virtuals)) {
1904
+ if (!isPlainObject12(virtuals)) {
1408
1905
  throw new Error(`Virtuals for model "${modelName}": virtuals must be a plain object mapping names to descriptors`);
1409
1906
  }
1410
1907
  const rootSchema = model?.schema ?? null;
@@ -1414,13 +1911,14 @@ var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName)
1414
1911
  { scopeVirtuals: rootVirtuals, storedFields: rootStored, scopePrefix: "", scopeLabel: "" },
1415
1912
  documentPermissionField
1416
1913
  );
1417
- const visitContainers = (currentVirtuals, currentSchema, currentPrefix) => {
1914
+ const visitContainers = (currentVirtuals, currentSchema, currentPrefix, schemaPrefix = "") => {
1418
1915
  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);
1916
+ if (raw === void 0 || !isVirtualContainerValue2(raw)) continue;
1917
+ const record2 = raw;
1918
+ const schemaPath = virtualFullPath(schemaPrefix, containerName);
1919
+ const childSchema = getChildSchemaForField(currentSchema, schemaPath);
1920
+ const nestedFields = getNestedChildFields(currentSchema, schemaPath);
1921
+ const currentStored = schemaPrefix ? getNestedChildFields(currentSchema, schemaPrefix) : collectStoredTopLevel(currentSchema);
1424
1922
  if (!currentStored.has(containerName)) {
1425
1923
  throw new Error(
1426
1924
  `Virtual ${containerName}: scope-container "${containerName}" is not a persisted field in its scope`
@@ -1434,7 +1932,7 @@ var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName)
1434
1932
  }
1435
1933
  const subStored = childSchema ? collectStoredTopLevel(childSchema) : nestedFields;
1436
1934
  const subPrefix = currentPrefix ? `${currentPrefix}.${containerName}` : containerName;
1437
- const subVirtuals = record.sub ?? {};
1935
+ const subVirtuals = record2.sub ?? {};
1438
1936
  validateVirtualsScope(
1439
1937
  {
1440
1938
  scopeVirtuals: subVirtuals,
@@ -1444,37 +1942,11 @@ var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName)
1444
1942
  },
1445
1943
  documentPermissionField
1446
1944
  );
1447
- const nextSchema = childSchema;
1448
- visitContainers(subVirtuals, nextSchema, subPrefix);
1945
+ visitContainers(subVirtuals, childSchema ?? currentSchema, subPrefix, childSchema ? "" : schemaPath);
1449
1946
  }
1450
1947
  };
1451
1948
  visitContainers(rootVirtuals, rootSchema, "");
1452
1949
  };
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
1950
  var AccessRuntime = class {
1479
1951
  constructor(options = {}) {
1480
1952
  this.globalOptions = new OptionsManager(
@@ -1482,6 +1954,7 @@ var AccessRuntime = class {
1482
1954
  requestPermissionField: "_permissions",
1483
1955
  globalPermissions: () => ({}),
1484
1956
  requireRegisteredPopulateModels: true,
1957
+ requireReadOperationAccessForPopulateDependencies: true,
1485
1958
  logger: defaultLogger,
1486
1959
  requestComplexity: defaultRequestComplexity
1487
1960
  },
@@ -1654,9 +2127,16 @@ var AccessRuntime = class {
1654
2127
  }
1655
2128
  /** Shallowly replace supplied global values; nested objects such as requestComplexity are not deep-merged. */
1656
2129
  setGlobalOptions(options) {
1657
- this.globalOptions.assign(options);
2130
+ const next = { ...options };
2131
+ if ("requireReadOperationAccessForPopulateDependencies" in next)
2132
+ next.requireReadOperationAccessForPopulateDependencies = next.requireReadOperationAccessForPopulateDependencies !== false;
2133
+ this.globalOptions.assign(next);
1658
2134
  }
1659
2135
  setGlobalOption(key, value) {
2136
+ if (key === "requireReadOperationAccessForPopulateDependencies") {
2137
+ this.globalOptions.set("requireReadOperationAccessForPopulateDependencies", value !== false);
2138
+ return;
2139
+ }
1660
2140
  this.globalOptions.set(key, value);
1661
2141
  }
1662
2142
  getGlobalOptions() {
@@ -1736,13 +2216,14 @@ var AccessRuntime = class {
1736
2216
  setModelOptions(modelName, options) {
1737
2217
  assertPublicDefaults(options.defaults);
1738
2218
  if ("hydration" in options) options = { ...options, hydration: normalizeHydration(options.hydration) };
1739
- const manager = this.getOrCreateModelOptions(modelName);
1740
2219
  const defaultOptions = this.getDefaultModelOptions();
1741
- const currentModelOptions = manager.fetch();
2220
+ const currentModelOptions = this.modelOptions[modelName]?.fetch() ?? { ...defaultModelOptions, modelName };
1742
2221
  const merged = { ...defaultOptions, ...currentModelOptions, ...options };
1743
2222
  const model = this.getModelInstance(modelName);
1744
2223
  const permissionField = merged.documentPermissionField ?? currentModelOptions.documentPermissionField ?? this.getDefaultModelOption("documentPermissionField") ?? "_permissions";
1745
2224
  validateVirtualsTree(model, merged.virtuals, permissionField, modelName);
2225
+ captureVirtualPopulateDependencies(this, modelName, merged);
2226
+ const manager = this.getOrCreateModelOptions(modelName);
1746
2227
  manager.assign({ ...defaultOptions, ...currentModelOptions, ...options });
1747
2228
  }
1748
2229
  setModelOption(modelName, key, value) {
@@ -1758,11 +2239,11 @@ var AccessRuntime = class {
1758
2239
  set3(proposed, keyStr.slice("defaults.".length), value);
1759
2240
  assertPublicDefaults(proposed);
1760
2241
  }
1761
- const manager = this.getOrCreateModelOptions(modelName);
2242
+ const manager = this.modelOptions[modelName];
1762
2243
  if (keyStr === "virtuals" || keyStr.startsWith("virtuals.") || keyStr === "documentPermissionField") {
1763
2244
  const model = this.getModelInstance(modelName);
1764
- const currentVirtuals = manager.get("virtuals");
1765
- const currentPermissionField = manager.get("documentPermissionField") ?? this.getDefaultModelOption("documentPermissionField") ?? "_permissions";
2245
+ const currentVirtuals = manager?.get("virtuals");
2246
+ const currentPermissionField = manager?.get("documentPermissionField") ?? this.getDefaultModelOption("documentPermissionField") ?? "_permissions";
1766
2247
  let proposedVirtuals = currentVirtuals;
1767
2248
  let proposedPermissionField = currentPermissionField;
1768
2249
  if (keyStr === "documentPermissionField") {
@@ -1776,10 +2257,10 @@ var AccessRuntime = class {
1776
2257
  const segments = nestedPath.split(".");
1777
2258
  let cursor = cloned;
1778
2259
  for (let i = 0; i < segments.length - 1; i++) {
1779
- if (!isPlainObject10(cursor)) break;
2260
+ if (!isPlainObject12(cursor)) break;
1780
2261
  cursor = cursor[segments[i]];
1781
2262
  }
1782
- if (isPlainObject10(cursor)) {
2263
+ if (isPlainObject12(cursor)) {
1783
2264
  delete cursor[segments[segments.length - 1]];
1784
2265
  }
1785
2266
  proposedVirtuals = cloned;
@@ -1789,8 +2270,14 @@ var AccessRuntime = class {
1789
2270
  }
1790
2271
  }
1791
2272
  validateVirtualsTree(model, proposedVirtuals, proposedPermissionField, modelName);
2273
+ captureVirtualPopulateDependencies(this, modelName, {
2274
+ ...this.getDefaultModelOptions(),
2275
+ ...manager?.fetch(),
2276
+ virtuals: proposedVirtuals,
2277
+ documentPermissionField: proposedPermissionField
2278
+ });
1792
2279
  }
1793
- manager.set(key, value);
2280
+ this.getOrCreateModelOptions(modelName).set(key, value);
1794
2281
  }
1795
2282
  /** Frozen stored snapshot, not fully resolved per-path defaults. Read again for current configuration. */
1796
2283
  getModelOptions(modelName) {
@@ -1829,7 +2316,7 @@ var AccessRuntime = class {
1829
2316
  const rootVirtuals = manager.get("virtuals");
1830
2317
  const scopeVirtuals = getScopeVirtualsObject(rootVirtuals, scopePath);
1831
2318
  if (!scopeVirtuals) return [];
1832
- return Object.entries(scopeVirtuals).filter(([, v]) => v !== void 0 && !isVirtualContainerValue(v)).map(([k]) => k);
2319
+ return Object.entries(scopeVirtuals).filter(([, v]) => v !== void 0 && !isVirtualContainerValue2(v)).map(([k]) => k);
1833
2320
  }
1834
2321
  /**
1835
2322
  * Whether a field is a registered virtual leaf in a scope (VIRT-01).
@@ -1843,7 +2330,7 @@ var AccessRuntime = class {
1843
2330
  const scopeVirtuals = getScopeVirtualsObject(rootVirtuals, scopePath);
1844
2331
  if (!scopeVirtuals || !(fieldName in scopeVirtuals)) return false;
1845
2332
  const entry = scopeVirtuals[fieldName];
1846
- if (entry === void 0 || isVirtualContainerValue(entry)) return false;
2333
+ if (entry === void 0 || isVirtualContainerValue2(entry)) return false;
1847
2334
  return true;
1848
2335
  }
1849
2336
  /**
@@ -1853,34 +2340,14 @@ var AccessRuntime = class {
1853
2340
  * interpreted as a getter descriptor when the access is absent: an
1854
2341
  * inapplicable registered name stays virtual (excluded from persisted
1855
2342
  * projections, write admission, and DB sort/filter/distinct).
1856
- * Shared record-level `dependsOn` is inherited when a leaf lacks its own.
2343
+ * Shared `dependsOn` and `dependsOnPopulate` lists inherit independently;
2344
+ * each defined leaf list replaces its shared list wholesale.
1857
2345
  */
1858
2346
  resolveVirtualDescriptor(modelName, virtualName, access, scopePath) {
1859
2347
  const manager = this.getOrCreateModelOptions(modelName);
1860
2348
  const rootVirtuals = manager.get("virtuals");
1861
2349
  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;
2350
+ return resolveSnapshotDescriptor(scopeVirtuals, virtualName, access);
1884
2351
  }
1885
2352
  getModelNames() {
1886
2353
  return Object.keys(this.modelOptions);
@@ -1963,16 +2430,16 @@ var AccessRuntime = class {
1963
2430
  }
1964
2431
  getModelRef(modelName, refPath) {
1965
2432
  this.ensureModelMeta(modelName);
1966
- const value = get2(this.modelRefs, `${modelName}.${refPath}`, null);
2433
+ const value = get3(this.modelRefs, `${modelName}.${refPath}`, null);
1967
2434
  return isString3(value) ? value : null;
1968
2435
  }
1969
2436
  getModelSub(modelName) {
1970
2437
  this.ensureModelMeta(modelName);
1971
- return get2(this.modelSubs, modelName, []);
2438
+ return get3(this.modelSubs, modelName, []);
1972
2439
  }
1973
2440
  getModelAtt(modelName) {
1974
2441
  this.ensureModelMeta(modelName);
1975
- return get2(this.modelAtts, modelName, []);
2442
+ return get3(this.modelAtts, modelName, []);
1976
2443
  }
1977
2444
  };
1978
2445
  var defaultRuntime = new AccessRuntime({ allowGlobalModelLookup: true });
@@ -1992,7 +2459,7 @@ var getGlobalOption = (key, defaultValue) => {
1992
2459
 
1993
2460
  // src/validation/common.ts
1994
2461
  import { z as z2 } from "zod";
1995
- import { isPlainObject as isPlainObject12 } from "@web-ts-toolkit/utils";
2462
+ import { isPlainObject as isPlainObject14 } from "@web-ts-toolkit/utils";
1996
2463
 
1997
2464
  // src/validation/public-admission-schema.ts
1998
2465
  var admissions = /* @__PURE__ */ new WeakMap();
@@ -2014,7 +2481,7 @@ function assertTransformedPublicAdmission(schema, value) {
2014
2481
  }
2015
2482
 
2016
2483
  // src/correlated-includes.ts
2017
- import { cloneDeep, isArray as isArray5, isPlainObject as isPlainObject11 } from "@web-ts-toolkit/utils";
2484
+ import { cloneDeep, isArray as isArray5, isPlainObject as isPlainObject13 } from "@web-ts-toolkit/utils";
2018
2485
  var PARENT_MARKER_KEY = "$parent";
2019
2486
  var ESCAPE_WRAPPER_KEY = "$escape";
2020
2487
  var SUBQUERY_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["$$sq", "$$date"]);
@@ -2025,10 +2492,10 @@ var FIELD_SEGMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
2025
2492
  var ARRAY_INDEX_SEGMENT_PATTERN = /^(0|[1-9]\d*)$/;
2026
2493
  var ownKeys = (value) => Object.keys(value);
2027
2494
  function isParentRefMarker(value) {
2028
- return isPlainObject11(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === PARENT_MARKER_KEY;
2495
+ return isPlainObject13(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === PARENT_MARKER_KEY;
2029
2496
  }
2030
2497
  function isEscapedParentRef(value) {
2031
- return isPlainObject11(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === ESCAPE_WRAPPER_KEY;
2498
+ return isPlainObject13(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === ESCAPE_WRAPPER_KEY;
2032
2499
  }
2033
2500
  function hasMarkerKey(value) {
2034
2501
  return ownKeys(value).includes(PARENT_MARKER_KEY) || ownKeys(value).includes(ESCAPE_WRAPPER_KEY);
@@ -2052,7 +2519,7 @@ function containsParentMarkerShape(value) {
2052
2519
  seen.add(current);
2053
2520
  return current.some(visit);
2054
2521
  }
2055
- if (isPlainObject11(current)) {
2522
+ if (isPlainObject13(current)) {
2056
2523
  if (seen.has(current)) return false;
2057
2524
  seen.add(current);
2058
2525
  if (hasMarkerKey(current)) return true;
@@ -2082,7 +2549,7 @@ function scanTemplateValueNode(value, path, issues) {
2082
2549
  }
2083
2550
  return;
2084
2551
  }
2085
- if (isPlainObject11(value)) {
2552
+ if (isPlainObject13(value)) {
2086
2553
  if (hasMarkerKey(value)) {
2087
2554
  issues.push({
2088
2555
  message: 'Malformed $parent reference: objects carrying $parent must be exactly { "$parent": "<field>" }',
@@ -2147,7 +2614,7 @@ function scanTemplateObjectEntries(obj, basePath, issues) {
2147
2614
  });
2148
2615
  return;
2149
2616
  }
2150
- if (isPlainObject11(clause)) {
2617
+ if (isPlainObject13(clause)) {
2151
2618
  scanTemplateObjectEntries(clause, clausePath, issues);
2152
2619
  }
2153
2620
  });
@@ -2158,7 +2625,7 @@ function scanTemplateObjectEntries(obj, basePath, issues) {
2158
2625
  }
2159
2626
  function validateCorrelatedFilterTemplate(filter) {
2160
2627
  const issues = [];
2161
- if (!isPlainObject11(filter)) {
2628
+ if (!isPlainObject13(filter)) {
2162
2629
  issues.push({ message: "Correlated include filter must be an object", path: [] });
2163
2630
  return issues;
2164
2631
  }
@@ -2286,7 +2753,7 @@ var correlatedFilterSchema = z2.record(z2.string(), z2.unknown()).superRefine((f
2286
2753
  }
2287
2754
  });
2288
2755
  var correlatedEmptyOptionsSchema = z2.unknown().refine(
2289
- (value) => value === void 0 || isPlainObject12(value) && Object.keys(value).length === 0,
2756
+ (value) => value === void 0 || isPlainObject14(value) && Object.keys(value).length === 0,
2290
2757
  "Correlated include options must be absent or empty"
2291
2758
  ).optional();
2292
2759
  var correlatedPathSchema = z2.string().min(1).superRefine((path, ctx) => {
@@ -2323,7 +2790,7 @@ var correlatedListArgsSchema = z2.object({
2323
2790
  include: z2.lazy(() => correlatedNestedIncludeSchema).optional()
2324
2791
  }).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
2325
2792
  var correlatedCountArgsSchema = z2.unknown().refine(
2326
- (value) => value === void 0 || isPlainObject12(value) && Object.keys(value).length === 0,
2793
+ (value) => value === void 0 || isPlainObject14(value) && Object.keys(value).length === 0,
2327
2794
  "Count includes accept no args"
2328
2795
  ).optional();
2329
2796
  var correlatedReadByIdSchema = z2.object({
@@ -2376,7 +2843,7 @@ var correlatedNestedIncludeSchema = z2.union([includeItemSchema, z2.array(includ
2376
2843
  var rejectDuplicateCorrelatedPaths = (items, ctx) => {
2377
2844
  const seen = /* @__PURE__ */ new Set();
2378
2845
  items.forEach((item, index) => {
2379
- if (isPlainObject12(item) && item.mode === "correlated" && typeof item.path === "string") {
2846
+ if (isPlainObject14(item) && item.mode === "correlated" && typeof item.path === "string") {
2380
2847
  if (seen.has(item.path)) {
2381
2848
  ctx.addIssue({
2382
2849
  code: z2.ZodIssueCode.custom,
@@ -2414,7 +2881,7 @@ function rejectKeys(body, ctx, keys2) {
2414
2881
  }
2415
2882
 
2416
2883
  // src/validation/parsers.ts
2417
- var clientErrors = JsonRouter2.clientErrors;
2884
+ var clientErrors = JsonRouter3.clientErrors;
2418
2885
  function parsePathParam(value, parameter) {
2419
2886
  const result = stringOrStringArray.safeParse(value);
2420
2887
  if (!result.success) {
@@ -2846,8 +3313,8 @@ function isArkTypeErrors(value) {
2846
3313
  if (typeof value !== "object" || value === null) {
2847
3314
  return false;
2848
3315
  }
2849
- const record = value;
2850
- return record.arkKind === "errors" || record[" arkKind"] === "errors";
3316
+ const record2 = value;
3317
+ return record2.arkKind === "errors" || record2[" arkKind"] === "errors";
2851
3318
  }
2852
3319
  function isThenable(value) {
2853
3320
  return (typeof value === "object" || typeof value === "function") && value !== null && "then" in value && typeof value.then === "function";