@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.js CHANGED
@@ -123,10 +123,10 @@ var FilterOperator = /* @__PURE__ */ ((FilterOperator2) => {
123
123
  })(FilterOperator || {});
124
124
 
125
125
  // src/validation/parsers.ts
126
- var import_express_json_router2 = __toESM(require("@web-ts-toolkit/express-json-router"));
126
+ var import_express_json_router3 = __toESM(require("@web-ts-toolkit/express-json-router"));
127
127
 
128
128
  // src/runtime.ts
129
- var import_mongoose2 = __toESM(require("mongoose"));
129
+ var import_mongoose3 = __toESM(require("mongoose"));
130
130
 
131
131
  // src/validation/public-admission.ts
132
132
  var import_express_json_router = __toESM(require("@web-ts-toolkit/express-json-router"));
@@ -169,10 +169,10 @@ function assertPublicAdmission(input, kind = "envelope") {
169
169
  const add = (child, childKind, key) => {
170
170
  pending.push({ value: child, kind: childKind, path: `${path}.${key}` });
171
171
  };
172
- const record = value;
172
+ const record2 = value;
173
173
  if (slot !== "data") {
174
174
  for (const key of privateControls) {
175
- if (key in record) {
175
+ if (key in record2) {
176
176
  throw new PublicAdmissionError(
177
177
  `Unsupported public control ${path}.${key}; use explicit per-call internal service options for backend authorization`
178
178
  );
@@ -188,16 +188,16 @@ function assertPublicAdmission(input, kind = "envelope") {
188
188
  for (const [key, child] of Object.entries(value)) add(child, key === "$$sq" ? "envelope" : "data", key);
189
189
  continue;
190
190
  }
191
- for (const key of ["args", "options", "sqOptions"]) add(record[key], "envelope", key);
192
- add(record.include, "include", "include");
193
- add(record.populate, "populate", "populate");
194
- for (const key of ["filter", "match", "data"]) add(record[key], "data", key);
191
+ for (const key of ["args", "options", "sqOptions"]) add(record2[key], "envelope", key);
192
+ add(record2.include, "include", "include");
193
+ add(record2.populate, "populate", "populate");
194
+ for (const key of ["filter", "match", "data"]) add(record2[key], "data", key);
195
195
  }
196
196
  }
197
197
  function assertHydrationDefaults(defaults) {
198
198
  if (defaults && typeof defaults === "object") {
199
- const record = defaults;
200
- if ("hydration" in record)
199
+ const record2 = defaults;
200
+ if ("hydration" in record2)
201
201
  throw new TypeError("defaults.hydration is unsupported; use model hydration configuration");
202
202
  for (const bucket of [
203
203
  "publicListOptions",
@@ -213,7 +213,7 @@ function assertHydrationDefaults(defaults) {
213
213
  "publicCreateOptions",
214
214
  "publicUpdateOptions"
215
215
  ]) {
216
- const options = record[bucket];
216
+ const options = record2[bucket];
217
217
  if (!options || typeof options !== "object" && typeof options !== "function") continue;
218
218
  for (const key of ["lean", "hydration"]) {
219
219
  if (!(key in options)) continue;
@@ -260,11 +260,28 @@ function normalizeHydrationUpdate(current, key, value) {
260
260
 
261
261
  // src/runtime.ts
262
262
  var import_mongoose_schema_jsonschema = __toESM(require("mongoose-schema-jsonschema"));
263
- var import_utils10 = require("@web-ts-toolkit/utils");
263
+ var import_utils12 = require("@web-ts-toolkit/utils");
264
264
 
265
265
  // src/helpers/query.ts
266
266
  var import_utils2 = require("@web-ts-toolkit/utils");
267
267
  var DEFAULT_LIST_HARD_LIMIT = 1e3;
268
+ function normalizeSelect(select) {
269
+ if (Array.isArray(select)) return (0, import_utils2.flattenDeep)(select.map(normalizeSelect));
270
+ if ((0, import_utils2.isPlainObject)(select)) {
271
+ return (0, import_utils2.reduce)(
272
+ select,
273
+ (ret, val, key) => {
274
+ if (val === 1) ret.push(key);
275
+ else if (val === -1) ret.push(`-${key}`);
276
+ return ret;
277
+ },
278
+ []
279
+ );
280
+ }
281
+ if ((0, import_utils2.isString)(select))
282
+ return String(select).split(/[\s,]+/).map((v) => v.trim()).filter(Boolean);
283
+ return [];
284
+ }
268
285
 
269
286
  // src/helpers/sort-policy.ts
270
287
  var fieldPathPattern = /^[A-Za-z_][A-Za-z0-9_$]*(\.[A-Za-z_][A-Za-z0-9_$]*)*$/;
@@ -1180,14 +1197,490 @@ function validateRequestComplexity(value, options, scope = "request") {
1180
1197
  return errors;
1181
1198
  }
1182
1199
 
1200
+ // src/acl/virtual-descriptor.ts
1201
+ var import_utils10 = require("@web-ts-toolkit/utils");
1202
+ var isVirtualContainerValue = (value) => (0, import_utils10.isPlainObject)(value) && "sub" in value;
1203
+ function getScopeVirtualsObject(root, scopePath = []) {
1204
+ let current = root;
1205
+ for (let i = 0; i < scopePath.length; i += 2) {
1206
+ if (!current || scopePath[i + 1] !== "sub") return null;
1207
+ const container = current[scopePath[i]];
1208
+ if (!isVirtualContainerValue(container)) return null;
1209
+ current = container.sub;
1210
+ }
1211
+ return (0, import_utils10.isPlainObject)(current) ? current : null;
1212
+ }
1213
+ function toDescriptor(leaf, shared = {}) {
1214
+ const record2 = (0, import_utils10.isFunction)(leaf) ? { get: leaf } : (0, import_utils10.isPlainObject)(leaf) ? leaf : null;
1215
+ if (!record2 || !(0, import_utils10.isFunction)(record2.get)) return void 0;
1216
+ const dependsOn = record2.dependsOn ?? shared.dependsOn ?? [];
1217
+ const populated = record2.dependsOnPopulate === void 0 ? shared.dependsOnPopulate : record2.dependsOnPopulate;
1218
+ return {
1219
+ get: record2.get,
1220
+ dependsOn: [...dependsOn],
1221
+ ...populated === void 0 ? {} : {
1222
+ dependsOnPopulate: populated.map(({ path, select }) => ({
1223
+ path,
1224
+ ...select === void 0 ? {} : { select: Array.isArray(select) ? [...select] : typeof select === "object" ? { ...select } : select }
1225
+ }))
1226
+ }
1227
+ };
1228
+ }
1229
+ function resolveSnapshotDescriptor(scopeVirtuals, virtualName, access) {
1230
+ const entry = scopeVirtuals?.[virtualName];
1231
+ if (entry === void 0 || isVirtualContainerValue(entry)) return void 0;
1232
+ if ((0, import_utils10.isFunction)(entry)) return toDescriptor(entry);
1233
+ if (!(0, import_utils10.isPlainObject)(entry)) return void 0;
1234
+ const record2 = entry;
1235
+ if ("get" in record2) return toDescriptor(record2);
1236
+ return toDescriptor(record2[access], record2) ?? toDescriptor(record2.default, record2);
1237
+ }
1238
+
1239
+ // src/acl/virtual-populate-dependencies.ts
1240
+ var import_utils11 = require("@web-ts-toolkit/utils");
1241
+
1242
+ // src/acl/authorization-snapshot.ts
1243
+ var import_node_util3 = require("util");
1244
+ var import_mongoose2 = require("mongoose");
1245
+ var import_express_json_router2 = __toESM(require("@web-ts-toolkit/express-json-router"));
1246
+ var ServiceAuthorizationError = class extends import_express_json_router2.default.clientErrors.BadRequestError {
1247
+ constructor(detail) {
1248
+ const errors = [{ detail: `Service authorization: ${detail}` }];
1249
+ super("Bad Request", { errors });
1250
+ this.result = { success: false, kind: "error", code: "bad_request" /* BadRequest */, errors };
1251
+ }
1252
+ };
1253
+ function authorizationError(detail) {
1254
+ throw new ServiceAuthorizationError(detail);
1255
+ }
1256
+ var unsafeKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1257
+ function authorizationRecord(value, label) {
1258
+ if (!value || typeof value !== "object" || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
1259
+ authorizationError(`${label} must be a plain object`);
1260
+ }
1261
+ return value;
1262
+ }
1263
+ function copyAuthorizationValue(value, configuration = false) {
1264
+ const active = /* @__PURE__ */ new WeakSet();
1265
+ let nodes = 0;
1266
+ function copy(input, depth) {
1267
+ if (++nodes > 2e4 || depth > 100) authorizationError("snapshot exceeds structural limits");
1268
+ if (typeof input === "function") {
1269
+ if (configuration === true) return input;
1270
+ authorizationError("functions are not policy values");
1271
+ }
1272
+ if (typeof input === "symbol") authorizationError("symbols are not policy values");
1273
+ if (input === null || typeof input !== "object") return input;
1274
+ if (active.has(input)) authorizationError("cyclic input");
1275
+ active.add(input);
1276
+ try {
1277
+ if (input instanceof Date) return new Date(input.getTime());
1278
+ if (Buffer.isBuffer(input)) return Buffer.from(input);
1279
+ if (input instanceof RegExp) {
1280
+ const result2 = new RegExp(input.source, input.flags);
1281
+ result2.lastIndex = input.lastIndex;
1282
+ return result2;
1283
+ }
1284
+ if (input instanceof import_mongoose2.mongo.BSON.BSONValue) {
1285
+ try {
1286
+ return import_mongoose2.mongo.BSON.deserialize(import_mongoose2.mongo.BSON.serialize({ value: input }), {
1287
+ promoteValues: false,
1288
+ bsonRegExp: true
1289
+ }).value;
1290
+ } catch {
1291
+ authorizationError("unsupported BSON snapshot");
1292
+ }
1293
+ }
1294
+ if (configuration && input instanceof Map) {
1295
+ return new Map([...input].map(([key, entry]) => [copy(key, depth + 1), copy(entry, depth + 1)]));
1296
+ }
1297
+ if (configuration && input instanceof Set) return new Set([...input].map((entry) => copy(entry, depth + 1)));
1298
+ const array = Array.isArray(input);
1299
+ if (array && input.length > 2e4) authorizationError("snapshot array exceeds structural limits");
1300
+ if (!array) authorizationRecord(input, "snapshot");
1301
+ const result = array ? [] : {};
1302
+ for (const key of Reflect.ownKeys(input)) {
1303
+ if (array && key === "length") continue;
1304
+ if (typeof key !== "string" || unsafeKeys.has(key)) authorizationError("unsafe snapshot key");
1305
+ if (array && !/^(0|[1-9]\d*)$/.test(key)) authorizationError("invalid array property");
1306
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
1307
+ if (!("value" in descriptor) || !descriptor.enumerable && configuration !== "document")
1308
+ authorizationError("accessor/non-enumerable input");
1309
+ Object.defineProperty(result, key, {
1310
+ value: copy(descriptor.value, depth + 1),
1311
+ enumerable: true,
1312
+ writable: true,
1313
+ configurable: true
1314
+ });
1315
+ }
1316
+ if (array) result.length = input.length;
1317
+ return Object.freeze(result);
1318
+ } finally {
1319
+ active.delete(input);
1320
+ }
1321
+ }
1322
+ return copy(value, 0);
1323
+ }
1324
+
1325
+ // src/acl/field-catalog.ts
1326
+ 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);
1327
+ var isSafeOutputPath = (path) => path.split(".").every(safeSegment);
1328
+ var inputControlSegments = /* @__PURE__ */ new Set([
1329
+ "_permissions",
1330
+ "_view",
1331
+ "_edit",
1332
+ "documentAuthorization",
1333
+ "documentAuthorizationStage",
1334
+ "resolvedAuthorization",
1335
+ "execution",
1336
+ "executionContext",
1337
+ "serviceExecutionContext",
1338
+ "preparedQuery",
1339
+ "preparedFilter",
1340
+ "preparedModelQuery",
1341
+ "resolvedIdentifier",
1342
+ "skipParsing",
1343
+ "skipAuthorization",
1344
+ "alreadyAuthorized"
1345
+ ]);
1346
+ function isSafeInputPath(path, requestIdentity = false) {
1347
+ return isSafeOutputPath(path) && path.split(".").every((part) => !inputControlSegments.has(part) && (requestIdentity || part !== "_id"));
1348
+ }
1349
+ function matchesCatalogPattern(pattern, path, descendants = false) {
1350
+ const expected = pattern.split(".");
1351
+ const actual = path.split(".");
1352
+ return (descendants ? actual.length >= expected.length : actual.length === expected.length) && expected.every((part, index) => part === "$*" || part === actual[index]);
1353
+ }
1354
+ function captureAuthorizationFieldCatalog(schema, virtuals, documentPermissionField = "_permissions") {
1355
+ const paths = /* @__PURE__ */ new Set();
1356
+ const containers = /* @__PURE__ */ new Set();
1357
+ const opaque = /* @__PURE__ */ new Set();
1358
+ const references = /* @__PURE__ */ new Set();
1359
+ const virtualPaths = /* @__PURE__ */ new Set();
1360
+ const virtualScopes = /* @__PURE__ */ new Set();
1361
+ const schemaVirtualPaths = /* @__PURE__ */ new Set();
1362
+ const arrays = /* @__PURE__ */ new Set();
1363
+ const protectedPaths = /* @__PURE__ */ new Set([documentPermissionField, "_doc", "$__", "__v"]);
1364
+ const add = (path) => {
1365
+ if (path.split(".").every((part) => part === "$*" || safeSegment(part))) paths.add(path);
1366
+ };
1367
+ const ancestors = (path) => {
1368
+ const parts = path.split(".");
1369
+ for (let n = 1; n < parts.length; n++) {
1370
+ const parent = parts.slice(0, n).join(".");
1371
+ add(parent);
1372
+ containers.add(parent);
1373
+ }
1374
+ };
1375
+ const visitType = (type, path, active) => {
1376
+ add(path);
1377
+ ancestors(path);
1378
+ const element = type.$embeddedSchemaType ?? type.embeddedSchemaType ?? type.caster;
1379
+ if (type.instance === "Array" || type.$isMongooseDocumentArray) arrays.add(path);
1380
+ if (type.options?.ref || type.options?.refPath || element?.options?.ref || element?.options?.refPath) {
1381
+ references.add(path);
1382
+ return;
1383
+ }
1384
+ if (type.$isSchemaMap || type.instance === "Map") {
1385
+ const value = type.$__schemaType;
1386
+ if (value) visitType(value, `${path}.$*`, active);
1387
+ else opaque.add(path);
1388
+ containers.delete(path);
1389
+ return;
1390
+ }
1391
+ const child = type.schema ?? element?.schema;
1392
+ if (child) {
1393
+ containers.add(path);
1394
+ if (!active.has(child)) visitSchema(child, path, /* @__PURE__ */ new Set([...active, child]));
1395
+ return;
1396
+ }
1397
+ if (type.instance === "Mixed" || element?.instance === "Mixed") opaque.add(path);
1398
+ };
1399
+ const visitSchema = (current, prefix, active) => {
1400
+ const version = current.options.versionKey;
1401
+ if (typeof version === "string") protectedPaths.add(prefix ? `${prefix}.${version}` : version);
1402
+ for (const name of Object.keys(current.virtuals)) {
1403
+ schemaVirtualPaths.add(prefix ? `${prefix}.${name}` : name);
1404
+ }
1405
+ for (const [name, raw] of Object.entries(current.paths)) {
1406
+ if (name.includes("$*")) continue;
1407
+ visitType(raw, prefix ? `${prefix}.${name}` : name, active);
1408
+ }
1409
+ };
1410
+ visitSchema(schema, "", /* @__PURE__ */ new Set([schema]));
1411
+ const persistedPaths = [...paths];
1412
+ const visitVirtuals = (record2, prefix) => {
1413
+ for (const [name, value] of Object.entries(record2 ?? {})) {
1414
+ if (value === void 0) continue;
1415
+ const path = prefix ? `${prefix}.${name}` : name;
1416
+ if (!isSafeOutputPath(path)) continue;
1417
+ if (value && typeof value === "object" && "sub" in value) {
1418
+ if (references.has(path)) continue;
1419
+ virtualScopes.add(path);
1420
+ add(path);
1421
+ containers.add(path);
1422
+ visitVirtuals(value.sub, path);
1423
+ } else {
1424
+ add(path);
1425
+ ancestors(path);
1426
+ virtualPaths.add(path);
1427
+ }
1428
+ }
1429
+ };
1430
+ visitVirtuals(virtuals, "");
1431
+ const eligible = [...paths].filter((p) => ![...protectedPaths].some((v) => matchesCatalogPattern(v, p, true)));
1432
+ return Object.freeze({
1433
+ paths: Object.freeze(eligible.filter((p) => !p.includes("$*"))),
1434
+ persistedPaths: Object.freeze(persistedPaths),
1435
+ arrayPaths: Object.freeze([...arrays]),
1436
+ schemaVirtualPaths: Object.freeze([...schemaVirtualPaths]),
1437
+ metadataPaths: Object.freeze([documentPermissionField]),
1438
+ dynamicPaths: Object.freeze(eligible.filter((p) => p.includes("$*"))),
1439
+ containerPaths: Object.freeze([...containers]),
1440
+ opaquePaths: Object.freeze([...opaque]),
1441
+ referencePaths: Object.freeze([...references]),
1442
+ virtualPaths: Object.freeze([...virtualPaths]),
1443
+ virtualScopePaths: Object.freeze([...virtualScopes]),
1444
+ protectedPaths: Object.freeze([...protectedPaths])
1445
+ });
1446
+ }
1447
+
1448
+ // src/acl/virtual-populate-dependencies.ts
1449
+ var elementType = (type) => type.$embeddedSchemaType ?? type.embeddedSchemaType ?? type.caster;
1450
+ var join = (prefix, path) => prefix ? `${prefix}.${path}` : path;
1451
+ var within = (path, parent) => path === parent || path.startsWith(`${parent}.`);
1452
+ var accesses = ["default", "list", "create", "read", "update"];
1453
+ var fail = (label, message) => {
1454
+ throw new Error(`Virtual ${label}: dependsOnPopulate ${message}`);
1455
+ };
1456
+ var varyingSchema = (schema) => Object.keys(schema.discriminators ?? {}).length > 0 || schema.discriminatorMapping?.isRoot === false;
1457
+ var varyingModel = (model) => !!model.baseModelName || Object.keys(model.discriminators ?? {}).length > 0 || varyingSchema(model.schema);
1458
+ function record(value, label) {
1459
+ if (!(0, import_utils11.isPlainObject)(value)) return fail(label, "entries must be plain { path, select? } objects");
1460
+ for (const key of Reflect.ownKeys(value)) {
1461
+ const property = Object.getOwnPropertyDescriptor(value, key);
1462
+ if (typeof key !== "string" || !property.enumerable || !("value" in property))
1463
+ fail(label, "requires enumerable data properties");
1464
+ }
1465
+ return value;
1466
+ }
1467
+ function targetSelectionCatalog(schema, options) {
1468
+ const leaves = /* @__PURE__ */ new Set();
1469
+ const containers = /* @__PURE__ */ new Set();
1470
+ const forbidden = /* @__PURE__ */ new Set([options.documentPermissionField ?? "_permissions"]);
1471
+ const visit = (current, prefix, active) => {
1472
+ const version = current.options.versionKey;
1473
+ if (typeof version === "string") forbidden.add(join(prefix, version));
1474
+ for (const name of Object.keys(current.virtuals)) forbidden.add(join(prefix, name));
1475
+ for (const name of Object.keys(current.aliases ?? {})) forbidden.add(join(prefix, name));
1476
+ for (const [name, raw] of Object.entries(current.paths)) {
1477
+ const path = join(prefix, name);
1478
+ if (!isSafeInputPath(path, true)) continue;
1479
+ const parts = path.split(".");
1480
+ for (let n = 1; n < parts.length; n++) containers.add(parts.slice(0, n).join("."));
1481
+ let type = raw;
1482
+ while (type.instance === "Array" && elementType(type)) type = elementType(type);
1483
+ if (type.instance === "Mixed" || type.instance === "Map") {
1484
+ forbidden.add(path);
1485
+ continue;
1486
+ }
1487
+ const child = type.schema ?? raw.schema;
1488
+ if (raw.options?.ref !== void 0 || raw.options?.refPath !== void 0 || type.options?.ref !== void 0 || type.options?.refPath !== void 0) {
1489
+ if (child) forbidden.add(path);
1490
+ else leaves.add(path);
1491
+ continue;
1492
+ }
1493
+ if (child) {
1494
+ if (active.has(child) || varyingSchema(child)) forbidden.add(path);
1495
+ else {
1496
+ containers.add(path);
1497
+ visit(child, path, /* @__PURE__ */ new Set([...active, child]));
1498
+ }
1499
+ } else leaves.add(path);
1500
+ }
1501
+ };
1502
+ visit(schema, "", /* @__PURE__ */ new Set([schema]));
1503
+ const visitVirtuals = (virtuals, prefix) => {
1504
+ for (const [name, entry] of Object.entries(virtuals)) {
1505
+ if (entry === void 0) continue;
1506
+ const path = join(prefix, name);
1507
+ if (isVirtualContainerValue(entry)) visitVirtuals(entry.sub, path);
1508
+ else forbidden.add(path);
1509
+ }
1510
+ };
1511
+ visitVirtuals(options.virtuals ?? {}, "");
1512
+ const allowed = (path) => ![...forbidden].some((blocked) => within(path, blocked));
1513
+ return {
1514
+ leaves: [...leaves].filter(allowed).sort(),
1515
+ paths: new Set([...leaves, ...containers].filter(allowed))
1516
+ };
1517
+ }
1518
+ function normalizeSelection(select, catalog, label) {
1519
+ if (select !== void 0 && typeof select !== "string") {
1520
+ if (Array.isArray(select)) {
1521
+ if (Array.from(select).some((token) => typeof token !== "string")) fail(label, "select arrays require strings");
1522
+ } else {
1523
+ const projection = record(select, label);
1524
+ for (const [path, value] of Object.entries(projection)) {
1525
+ if (!isSafeInputPath(path, true) || value !== 1 && value !== -1)
1526
+ fail(label, "select objects require safe persisted paths with 1/-1 values");
1527
+ }
1528
+ }
1529
+ }
1530
+ const tokens = normalizeSelect(select);
1531
+ for (const token of tokens) {
1532
+ const path = token.startsWith("-") ? token.slice(1) : token;
1533
+ if (!isSafeInputPath(path, true) || !catalog.paths.has(path))
1534
+ fail(label, `select path "${path}" is not an eligible static persisted target path`);
1535
+ }
1536
+ const positive = tokens.filter((token) => !token.startsWith("-"));
1537
+ const negative = tokens.filter((token) => token.startsWith("-")).map((token) => token.slice(1));
1538
+ const selection = catalog.leaves.filter(
1539
+ (path) => (path === "_id" || positive.length === 0 || positive.some((included) => within(path, included))) && !negative.some((excluded) => within(path, excluded))
1540
+ );
1541
+ return {
1542
+ selection: Object.freeze(selection),
1543
+ fetchSelection: Object.freeze([.../* @__PURE__ */ new Set(["_id", ...selection])].sort()),
1544
+ includeId: selection.includes("_id")
1545
+ };
1546
+ }
1547
+ function captureVirtualPopulateDependencies(runtime, modelName, options) {
1548
+ const model = runtime.getModelInstance(modelName);
1549
+ const targets = /* @__PURE__ */ new Map();
1550
+ const scopes = [];
1551
+ const visit = (virtuals, schema, schemaPrefix, scopePath, unsupportedScope) => {
1552
+ const sourcePrefix = scopePath.filter((_, index) => index % 2 === 0).join(".");
1553
+ const captureList = (value, label) => {
1554
+ if (value === void 0) return Object.freeze([]);
1555
+ if (!Array.isArray(value)) return fail(label, "must be an array");
1556
+ const seen = /* @__PURE__ */ new Set();
1557
+ return Object.freeze(
1558
+ Array.from(value, (entry) => {
1559
+ const dependency = record(entry, label);
1560
+ if (Object.keys(dependency).some((key) => key !== "path" && key !== "select"))
1561
+ fail(label, "accepts only path/select");
1562
+ const path = dependency.path;
1563
+ if (typeof path !== "string" || path.includes(".") || !isSafeInputPath(path, true))
1564
+ return fail(label, "path must be a safe scope-local persisted reference key");
1565
+ if (seen.has(path)) fail(label, `duplicate path "${path}"`);
1566
+ seen.add(path);
1567
+ if (within(join(sourcePrefix, path), options.documentPermissionField ?? "_permissions") || path === schema?.options.versionKey)
1568
+ fail(label, "source permission metadata/version paths are not reference inputs");
1569
+ if (!model || !schema || unsupportedScope)
1570
+ return fail(label, "requires a static embedded, non-discriminator source scope");
1571
+ const type = schema.path(join(schemaPrefix, path));
1572
+ const isArray6 = type?.instance === "Array";
1573
+ const scalar = type && (isArray6 ? elementType(type) : type);
1574
+ const ref = type?.options?.ref ?? scalar?.options?.ref;
1575
+ if (!scalar || scalar.instance !== "ObjectId" || typeof ref !== "string" || !ref || type?.options?.refPath !== void 0 || scalar.options?.refPath !== void 0)
1576
+ return fail(label, `path "${path}" requires a static string ref on an ObjectId scalar or 1D array`);
1577
+ let captured = targets.get(ref);
1578
+ if (!captured) {
1579
+ if (ref !== modelName && !runtime.hasModel(ref))
1580
+ return fail(label, `target "${ref}" must be configured first in this runtime`);
1581
+ const targetModel = runtime.getModelInstance(ref);
1582
+ if (!targetModel || targetModel.db !== model.db || model.db.models[ref] !== targetModel || varyingModel(targetModel) || targetModel.schema.path("_id")?.instance !== "ObjectId")
1583
+ return fail(
1584
+ label,
1585
+ `target "${ref}" requires a registered same-connection non-discriminator ObjectId model bound to the source ref`
1586
+ );
1587
+ const targetOptions = ref === modelName ? options : runtime.getModelOptions(ref);
1588
+ const defaults = runtime.getDefaultModelOptions();
1589
+ const readOption = (key) => resolveNestedOption((path2, fallback) => (0, import_utils11.get)(targetOptions, path2, fallback), key, (0, import_utils11.get)(defaults, key));
1590
+ const documentPermissionField = targetOptions.documentPermissionField ?? defaults.documentPermissionField;
1591
+ const catalog = captureAuthorizationFieldCatalog(targetModel.schema, void 0, documentPermissionField);
1592
+ const configuration = copyAuthorizationValue(
1593
+ {
1594
+ catalog,
1595
+ documentPermissionField,
1596
+ operationAccess: { read: readOption("operationAccess.read") },
1597
+ baseFilter: { read: readOption("baseFilter.read") },
1598
+ overrideFilter: { read: readOption("overrideFilter.read") },
1599
+ hydration: targetOptions.hydration
1600
+ },
1601
+ true
1602
+ );
1603
+ captured = {
1604
+ target: Object.freeze({
1605
+ identity: Object.freeze({}),
1606
+ runtime,
1607
+ modelName: ref,
1608
+ model: targetModel,
1609
+ schema: targetModel.schema,
1610
+ configuration
1611
+ }),
1612
+ catalog: targetSelectionCatalog(targetModel.schema, { ...targetOptions, documentPermissionField })
1613
+ };
1614
+ targets.set(ref, captured);
1615
+ }
1616
+ return Object.freeze({
1617
+ path,
1618
+ sourcePath: join(sourcePrefix, path),
1619
+ cardinality: isArray6 ? "array" : "scalar",
1620
+ target: captured.target,
1621
+ ...normalizeSelection(dependency.select, captured.catalog, label)
1622
+ });
1623
+ })
1624
+ );
1625
+ };
1626
+ for (const [name, raw] of Object.entries(virtuals)) {
1627
+ if (isVirtualContainerValue(raw)) {
1628
+ const path = join(schemaPrefix, name);
1629
+ const type = schema?.path(path);
1630
+ const element = type && elementType(type);
1631
+ const child = type?.schema ?? element?.schema;
1632
+ const nonEmbedded = [type, element].some(
1633
+ (entry2) => entry2 && (entry2.options?.ref !== void 0 || entry2.options?.refPath !== void 0 || entry2.instance === "Map" || entry2.instance === "Mixed")
1634
+ );
1635
+ visit(
1636
+ raw.sub,
1637
+ child ?? schema,
1638
+ child ? "" : path,
1639
+ [...scopePath, name, "sub"],
1640
+ unsupportedScope || nonEmbedded || !!(child && varyingSchema(child))
1641
+ );
1642
+ continue;
1643
+ }
1644
+ if (!(0, import_utils11.isPlainObject)(raw)) continue;
1645
+ const entry = raw;
1646
+ const label = join(sourcePrefix, name);
1647
+ captureList(entry.dependsOnPopulate, label);
1648
+ if (!("get" in entry))
1649
+ for (const access of accesses) {
1650
+ if ((0, import_utils11.isPlainObject)(entry[access]))
1651
+ captureList(entry[access].dependsOnPopulate, `${label}.${access}`);
1652
+ }
1653
+ const byAccess = {};
1654
+ let declared = false;
1655
+ for (const access of accesses) {
1656
+ const descriptor = resolveSnapshotDescriptor(virtuals, name, access);
1657
+ if (descriptor) {
1658
+ byAccess[access] = captureList(descriptor.dependsOnPopulate, `${label}.${access}`);
1659
+ declared ||= descriptor.dependsOnPopulate !== void 0;
1660
+ }
1661
+ }
1662
+ if (declared)
1663
+ scopes.push(
1664
+ Object.freeze({
1665
+ scopePath: Object.freeze([...scopePath]),
1666
+ virtualName: name,
1667
+ byAccess: Object.freeze(byAccess)
1668
+ })
1669
+ );
1670
+ }
1671
+ };
1672
+ visit(options.virtuals ?? {}, model?.schema, "", [], !!model && varyingModel(model));
1673
+ return Object.freeze({ scopes: Object.freeze(scopes) });
1674
+ }
1675
+
1183
1676
  // src/runtime.ts
1184
1677
  var mongooseJsonSchemaInitialized = false;
1185
1678
  var ensureMongooseJsonSchemaInitialized = () => {
1186
1679
  if (mongooseJsonSchemaInitialized) return;
1187
- (0, import_mongoose_schema_jsonschema.default)(import_mongoose2.default);
1680
+ (0, import_mongoose_schema_jsonschema.default)(import_mongoose3.default);
1188
1681
  mongooseJsonSchemaInitialized = true;
1189
1682
  };
1190
- var pluralize = import_mongoose2.default.pluralize();
1683
+ var pluralize = import_mongoose3.default.pluralize();
1191
1684
  var FIELD_ACCESS_KEYS = ["list", "create", "read", "update"];
1192
1685
  var defaultModelOptions = {
1193
1686
  basePath: null,
@@ -1200,44 +1693,44 @@ var defaultDataOptions = {
1200
1693
  listHardLimit: DEFAULT_LIST_HARD_LIMIT
1201
1694
  };
1202
1695
  var normalizeBasePath = (name, value) => {
1203
- if ((0, import_utils10.isNil)(value)) {
1696
+ if ((0, import_utils12.isNil)(value)) {
1204
1697
  return `/${pluralize(name)}`;
1205
1698
  }
1206
- return (0, import_utils10.isString)(value) ? (0, import_utils10.addLeadingSlash)(value) : "";
1699
+ return (0, import_utils12.isString)(value) ? (0, import_utils12.addLeadingSlash)(value) : "";
1207
1700
  };
1208
1701
  var hasModelPermission = (value, modelPermissionPrefix) => {
1209
1702
  if (!modelPermissionPrefix) {
1210
1703
  return true;
1211
1704
  }
1212
- if ((0, import_utils10.isString)(value)) {
1705
+ if ((0, import_utils12.isString)(value)) {
1213
1706
  return value.trim().split(" ").some((item) => item.startsWith(modelPermissionPrefix));
1214
1707
  }
1215
- if ((0, import_utils10.isArray)(value)) {
1708
+ if ((0, import_utils12.isArray)(value)) {
1216
1709
  return value.some((item) => {
1217
- if ((0, import_utils10.isString)(item) || (0, import_utils10.isArray)(item)) {
1710
+ if ((0, import_utils12.isString)(item) || (0, import_utils12.isArray)(item)) {
1218
1711
  return hasModelPermission(item, modelPermissionPrefix);
1219
1712
  }
1220
1713
  return true;
1221
1714
  });
1222
1715
  }
1223
- return (0, import_utils10.isFunction)(value);
1716
+ return (0, import_utils12.isFunction)(value);
1224
1717
  };
1225
1718
  var classifyPermissionSchema = (permissionSchema, modelPermissionPrefix) => {
1226
1719
  const schemaKeys = Object.keys(permissionSchema);
1227
1720
  const globalPermissionKeys = {};
1228
1721
  const modelPermissionKeys = {};
1229
- (0, import_utils10.forEach)(schemaKeys, (schemaKey) => {
1722
+ (0, import_utils12.forEach)(schemaKeys, (schemaKey) => {
1230
1723
  const schemaRule = permissionSchema[schemaKey];
1231
- const ruleEntries = (0, import_utils10.isPlainObject)(schemaRule) ? Object.entries(schemaRule) : FIELD_ACCESS_KEYS.map((accessKey) => [accessKey, schemaRule]);
1724
+ const ruleEntries = (0, import_utils12.isPlainObject)(schemaRule) ? Object.entries(schemaRule) : FIELD_ACCESS_KEYS.map((accessKey) => [accessKey, schemaRule]);
1232
1725
  for (let x = 0; x < ruleEntries.length; x++) {
1233
1726
  const [accessKey, value] = ruleEntries[x];
1234
- if (!(0, import_utils10.isArray)(globalPermissionKeys[accessKey])) {
1727
+ if (!(0, import_utils12.isArray)(globalPermissionKeys[accessKey])) {
1235
1728
  globalPermissionKeys[accessKey] = [];
1236
1729
  }
1237
- if (!(0, import_utils10.isArray)(modelPermissionKeys[accessKey])) {
1730
+ if (!(0, import_utils12.isArray)(modelPermissionKeys[accessKey])) {
1238
1731
  modelPermissionKeys[accessKey] = [];
1239
1732
  }
1240
- if ((0, import_utils10.isBoolean)(value)) {
1733
+ if ((0, import_utils12.isBoolean)(value)) {
1241
1734
  globalPermissionKeys[accessKey].push(schemaKey);
1242
1735
  continue;
1243
1736
  }
@@ -1269,13 +1762,13 @@ var VIRTUAL_UNSUPPORTED_ACCESSES = ["delete", "distinct", "count"];
1269
1762
  var VIRTUAL_RESERVED_NAMES = /* @__PURE__ */ new Set(["_id", "__v", "id"]);
1270
1763
  var VIRTUAL_DANGEROUS_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1271
1764
  var VIRTUAL_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
1272
- var isVirtualContainerValue = (value) => (0, import_utils10.isPlainObject)(value) && "sub" in value;
1765
+ var isVirtualContainerValue2 = (value) => (0, import_utils12.isPlainObject)(value) && "sub" in value;
1273
1766
  var virtualFullPath = (prefix, name) => prefix ? `${prefix}.${name}` : name;
1274
1767
  var virtualPathsOverlap = (virtualPath, permissionField) => {
1275
1768
  if (!virtualPath || !permissionField) return false;
1276
1769
  try {
1277
- if ((0, import_utils10.get)((0, import_utils10.set)({}, virtualPath, true), permissionField) !== void 0) return true;
1278
- if ((0, import_utils10.get)((0, import_utils10.set)({}, permissionField, true), virtualPath) !== void 0) return true;
1770
+ if ((0, import_utils12.get)((0, import_utils12.set)({}, virtualPath, true), permissionField) !== void 0) return true;
1771
+ if ((0, import_utils12.get)((0, import_utils12.set)({}, permissionField, true), virtualPath) !== void 0) return true;
1279
1772
  } catch {
1280
1773
  return false;
1281
1774
  }
@@ -1336,12 +1829,12 @@ var cloneVirtualsForValidation = (value) => {
1336
1829
  };
1337
1830
  var validateVirtualDependsOn = (dependsOn, entryLabel, storedFields, siblingVirtualNames) => {
1338
1831
  if (dependsOn === void 0) return [];
1339
- if (!(0, import_utils10.isArray)(dependsOn)) {
1832
+ if (!(0, import_utils12.isArray)(dependsOn)) {
1340
1833
  throw new Error(`Virtual ${entryLabel}: dependsOn must be an array of top-level persisted field names`);
1341
1834
  }
1342
1835
  const list = dependsOn;
1343
1836
  for (const dep of list) {
1344
- if (!(0, import_utils10.isString)(dep) || dep.length === 0) {
1837
+ if (!(0, import_utils12.isString)(dep) || dep.length === 0) {
1345
1838
  throw new Error(`Virtual ${entryLabel}: dependsOn entries must be non-empty strings`);
1346
1839
  }
1347
1840
  if (dep.includes(".")) {
@@ -1363,18 +1856,20 @@ var validateVirtualDependsOn = (dependsOn, entryLabel, storedFields, siblingVirt
1363
1856
  return list;
1364
1857
  };
1365
1858
  var validateVirtualLeafValue = (value, entryLabel, storedFields, siblingVirtualNames, sharedDependsOn) => {
1366
- if ((0, import_utils10.isFunction)(value)) return;
1367
- if (!(0, import_utils10.isPlainObject)(value)) {
1859
+ if ((0, import_utils12.isFunction)(value)) return;
1860
+ if (!(0, import_utils12.isPlainObject)(value)) {
1368
1861
  throw new Error(`Virtual ${entryLabel}: descriptor must be a getter function or { get, dependsOn }`);
1369
1862
  }
1370
- const record = value;
1371
- if (!(0, import_utils10.isFunction)(record.get)) {
1863
+ const record2 = value;
1864
+ if (!(0, import_utils12.isFunction)(record2.get)) {
1372
1865
  throw new Error(`Virtual ${entryLabel}: descriptor.get must be a callable getter function`);
1373
1866
  }
1374
- validateVirtualDependsOn(record.dependsOn ?? sharedDependsOn, entryLabel, storedFields, siblingVirtualNames);
1375
- for (const k of Object.keys(record)) {
1376
- if (k !== "get" && k !== "dependsOn") {
1377
- throw new Error(`Virtual ${entryLabel}: unsupported descriptor key "${k}" (expected only get/dependsOn)`);
1867
+ validateVirtualDependsOn(record2.dependsOn ?? sharedDependsOn, entryLabel, storedFields, siblingVirtualNames);
1868
+ for (const k of Object.keys(record2)) {
1869
+ if (k !== "get" && k !== "dependsOn" && k !== "dependsOnPopulate") {
1870
+ throw new Error(
1871
+ `Virtual ${entryLabel}: unsupported descriptor key "${k}" (expected get/dependsOn/dependsOnPopulate)`
1872
+ );
1378
1873
  }
1379
1874
  }
1380
1875
  };
@@ -1383,7 +1878,7 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1383
1878
  const leafNames = /* @__PURE__ */ new Set();
1384
1879
  for (const [name, raw] of Object.entries(scopeVirtuals)) {
1385
1880
  if (raw === void 0) continue;
1386
- if (isVirtualContainerValue(raw)) continue;
1881
+ if (isVirtualContainerValue2(raw)) continue;
1387
1882
  leafNames.add(name);
1388
1883
  }
1389
1884
  for (const [name, raw] of Object.entries(scopeVirtuals)) {
@@ -1400,15 +1895,15 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1400
1895
  `Virtual ${scopeLabel}${name}: overlaps document permission field "${documentPermissionField}" (equal/ancestor/descendant rejected)`
1401
1896
  );
1402
1897
  }
1403
- if (isVirtualContainerValue(raw)) {
1404
- const record2 = raw;
1405
- const extra = Object.keys(record2).filter((k) => k !== "sub");
1898
+ if (isVirtualContainerValue2(raw)) {
1899
+ const record3 = raw;
1900
+ const extra = Object.keys(record3).filter((k) => k !== "sub");
1406
1901
  if (extra.length > 0) {
1407
1902
  throw new Error(
1408
1903
  `Virtual ${scopeLabel}${name}: scope-container must only hold "sub" (embedded container "${name}" is not a virtual-leaf collision)`
1409
1904
  );
1410
1905
  }
1411
- if (!(0, import_utils10.isPlainObject)(record2.sub)) {
1906
+ if (!(0, import_utils12.isPlainObject)(record3.sub)) {
1412
1907
  throw new Error(`Virtual ${scopeLabel}${name}: scope-container "sub" must be a plain object`);
1413
1908
  }
1414
1909
  continue;
@@ -1418,19 +1913,19 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1418
1913
  `Virtual ${scopeLabel}${name}: collides with a stored path (inspected actual Mongoose schema/child scope; getModelAtt alone is insufficient)`
1419
1914
  );
1420
1915
  }
1421
- if ((0, import_utils10.isFunction)(raw)) continue;
1422
- if (!(0, import_utils10.isPlainObject)(raw)) {
1916
+ if ((0, import_utils12.isFunction)(raw)) continue;
1917
+ if (!(0, import_utils12.isPlainObject)(raw)) {
1423
1918
  throw new Error(
1424
1919
  `Virtual ${scopeLabel}${name}: malformed descriptor (expected getter, { get, dependsOn }, or per-access record)`
1425
1920
  );
1426
1921
  }
1427
- const record = raw;
1428
- const hasGet = "get" in record;
1429
- const accessKeys = Object.keys(record).filter((k) => VIRTUAL_SUPPORTED_ACCESSES.includes(k));
1430
- const unsupported2 = Object.keys(record).filter(
1431
- (k) => k !== "get" && k !== "dependsOn" && k !== "sub" && !VIRTUAL_SUPPORTED_ACCESSES.includes(k)
1922
+ const record2 = raw;
1923
+ const hasGet = "get" in record2;
1924
+ const accessKeys = Object.keys(record2).filter((k) => VIRTUAL_SUPPORTED_ACCESSES.includes(k));
1925
+ const unsupported2 = Object.keys(record2).filter(
1926
+ (k) => k !== "get" && k !== "dependsOn" && k !== "dependsOnPopulate" && k !== "sub" && !VIRTUAL_SUPPORTED_ACCESSES.includes(k)
1432
1927
  );
1433
- for (const key of Object.keys(record)) {
1928
+ for (const key of Object.keys(record2)) {
1434
1929
  if (VIRTUAL_UNSUPPORTED_ACCESSES.includes(key)) {
1435
1930
  throw new Error(
1436
1931
  `Virtual ${scopeLabel}${name}: unsupported access "${key}" (supported: default + list/create/read/update; NOT delete/distinct/count)`
@@ -1448,21 +1943,21 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1448
1943
  `Virtual ${scopeLabel}${name}: cannot mix "get" with per-access keys (use either bare { get, dependsOn } or a per-access record)`
1449
1944
  );
1450
1945
  }
1451
- if ("sub" in record) {
1946
+ if ("sub" in record2) {
1452
1947
  throw new Error(`Virtual ${scopeLabel}${name}: cannot mix "get" with "sub" (leaf vs embedded container)`);
1453
1948
  }
1454
1949
  validateVirtualLeafValue(raw, `${scopeLabel}${name}`, storedFields, leafNames, void 0);
1455
1950
  continue;
1456
1951
  }
1457
1952
  if (accessKeys.length > 0) {
1458
- if ("sub" in record) {
1953
+ if ("sub" in record2) {
1459
1954
  throw new Error(`Virtual ${scopeLabel}${name}: cannot mix per-access keys with "sub"`);
1460
1955
  }
1461
- validateVirtualDependsOn(record.dependsOn, `${scopeLabel}${name}`, storedFields, leafNames);
1956
+ validateVirtualDependsOn(record2.dependsOn, `${scopeLabel}${name}`, storedFields, leafNames);
1462
1957
  for (const access of accessKeys) {
1463
- const leaf = record[access];
1958
+ const leaf = record2[access];
1464
1959
  if (leaf === void 0) continue;
1465
- validateVirtualLeafValue(leaf, `${scopeLabel}${name}.${access}`, storedFields, leafNames, record.dependsOn);
1960
+ validateVirtualLeafValue(leaf, `${scopeLabel}${name}.${access}`, storedFields, leafNames, record2.dependsOn);
1466
1961
  }
1467
1962
  continue;
1468
1963
  }
@@ -1473,7 +1968,7 @@ var validateVirtualsScope = (scope, documentPermissionField) => {
1473
1968
  };
1474
1969
  var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName) => {
1475
1970
  if (virtuals === void 0 || virtuals === null) return;
1476
- if (!(0, import_utils10.isPlainObject)(virtuals)) {
1971
+ if (!(0, import_utils12.isPlainObject)(virtuals)) {
1477
1972
  throw new Error(`Virtuals for model "${modelName}": virtuals must be a plain object mapping names to descriptors`);
1478
1973
  }
1479
1974
  const rootSchema = model?.schema ?? null;
@@ -1483,13 +1978,14 @@ var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName)
1483
1978
  { scopeVirtuals: rootVirtuals, storedFields: rootStored, scopePrefix: "", scopeLabel: "" },
1484
1979
  documentPermissionField
1485
1980
  );
1486
- const visitContainers = (currentVirtuals, currentSchema, currentPrefix) => {
1981
+ const visitContainers = (currentVirtuals, currentSchema, currentPrefix, schemaPrefix = "") => {
1487
1982
  for (const [containerName, raw] of Object.entries(currentVirtuals)) {
1488
- if (raw === void 0 || !isVirtualContainerValue(raw)) continue;
1489
- const record = raw;
1490
- const childSchema = getChildSchemaForField(currentSchema, containerName);
1491
- const nestedFields = getNestedChildFields(currentSchema, containerName);
1492
- const currentStored = collectStoredTopLevel(currentSchema);
1983
+ if (raw === void 0 || !isVirtualContainerValue2(raw)) continue;
1984
+ const record2 = raw;
1985
+ const schemaPath = virtualFullPath(schemaPrefix, containerName);
1986
+ const childSchema = getChildSchemaForField(currentSchema, schemaPath);
1987
+ const nestedFields = getNestedChildFields(currentSchema, schemaPath);
1988
+ const currentStored = schemaPrefix ? getNestedChildFields(currentSchema, schemaPrefix) : collectStoredTopLevel(currentSchema);
1493
1989
  if (!currentStored.has(containerName)) {
1494
1990
  throw new Error(
1495
1991
  `Virtual ${containerName}: scope-container "${containerName}" is not a persisted field in its scope`
@@ -1503,7 +1999,7 @@ var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName)
1503
1999
  }
1504
2000
  const subStored = childSchema ? collectStoredTopLevel(childSchema) : nestedFields;
1505
2001
  const subPrefix = currentPrefix ? `${currentPrefix}.${containerName}` : containerName;
1506
- const subVirtuals = record.sub ?? {};
2002
+ const subVirtuals = record2.sub ?? {};
1507
2003
  validateVirtualsScope(
1508
2004
  {
1509
2005
  scopeVirtuals: subVirtuals,
@@ -1513,37 +2009,11 @@ var validateVirtualsTree = (model, virtuals, documentPermissionField, modelName)
1513
2009
  },
1514
2010
  documentPermissionField
1515
2011
  );
1516
- const nextSchema = childSchema;
1517
- visitContainers(subVirtuals, nextSchema, subPrefix);
2012
+ visitContainers(subVirtuals, childSchema ?? currentSchema, subPrefix, childSchema ? "" : schemaPath);
1518
2013
  }
1519
2014
  };
1520
2015
  visitContainers(rootVirtuals, rootSchema, "");
1521
2016
  };
1522
- var getScopeVirtualsObject = (rootVirtuals, scopePath) => {
1523
- if (!rootVirtuals || !(0, import_utils10.isPlainObject)(rootVirtuals)) return null;
1524
- if (!scopePath || scopePath.length === 0) return rootVirtuals;
1525
- let current = rootVirtuals;
1526
- for (let i = 0; i < scopePath.length; i += 2) {
1527
- const field = scopePath[i];
1528
- const subMarker = scopePath[i + 1];
1529
- if (!field || subMarker !== "sub") return null;
1530
- if (!(0, import_utils10.isPlainObject)(current)) return null;
1531
- const container = current[field];
1532
- if (!isVirtualContainerValue(container)) return null;
1533
- current = container.sub;
1534
- if (!(0, import_utils10.isPlainObject)(current)) return null;
1535
- }
1536
- return current;
1537
- };
1538
- var toVirtualDescriptor = (leaf, sharedDependsOn) => {
1539
- if ((0, import_utils10.isFunction)(leaf))
1540
- return { get: leaf, dependsOn: sharedDependsOn ?? [] };
1541
- if (!(0, import_utils10.isPlainObject)(leaf)) return null;
1542
- const record = leaf;
1543
- if (!(0, import_utils10.isFunction)(record.get)) return null;
1544
- const dependsOn = record.dependsOn ?? sharedDependsOn ?? [];
1545
- return { get: record.get, dependsOn };
1546
- };
1547
2017
  var AccessRuntime = class {
1548
2018
  constructor(options = {}) {
1549
2019
  this.globalOptions = new OptionsManager(
@@ -1592,20 +2062,20 @@ var AccessRuntime = class {
1592
2062
  this.modelInstances[modelName] = model;
1593
2063
  }
1594
2064
  hasModelInstance(modelName) {
1595
- return modelName in this.modelInstances || this.allowGlobalModelLookup && modelName in import_mongoose2.default.models;
2065
+ return modelName in this.modelInstances || this.allowGlobalModelLookup && modelName in import_mongoose3.default.models;
1596
2066
  }
1597
2067
  getModelInstance(modelName) {
1598
2068
  const registered = this.modelInstances[modelName];
1599
2069
  if (registered) return registered;
1600
2070
  if (!this.allowGlobalModelLookup) return null;
1601
- const global = import_mongoose2.default.models[modelName];
2071
+ const global = import_mongoose3.default.models[modelName];
1602
2072
  if (!global) return null;
1603
2073
  this.registerModelInstance(modelName, global);
1604
2074
  return global;
1605
2075
  }
1606
2076
  registerOpenApiRoute(route) {
1607
2077
  this.openApiRegistry.register(
1608
- this.openApiPathPrefix ? { ...route, path: (0, import_utils10.normalizeUrlPath)(`${this.openApiPathPrefix}/${route.path}`) } : route
2078
+ this.openApiPathPrefix ? { ...route, path: (0, import_utils12.normalizeUrlPath)(`${this.openApiPathPrefix}/${route.path}`) } : route
1609
2079
  );
1610
2080
  }
1611
2081
  /**
@@ -1616,7 +2086,7 @@ var AccessRuntime = class {
1616
2086
  */
1617
2087
  withOpenApiPathPrefix(prefix, build) {
1618
2088
  const previous = this.openApiPathPrefix;
1619
- this.openApiPathPrefix = (0, import_utils10.normalizeUrlPath)(`${previous}/${prefix}`);
2089
+ this.openApiPathPrefix = (0, import_utils12.normalizeUrlPath)(`${previous}/${prefix}`);
1620
2090
  try {
1621
2091
  return build();
1622
2092
  } finally {
@@ -1752,7 +2222,7 @@ var AccessRuntime = class {
1752
2222
  if (keyStr === "defaults") assertPublicDefaults(value);
1753
2223
  else if (keyStr.startsWith("defaults.")) {
1754
2224
  const proposed = {};
1755
- (0, import_utils10.set)(proposed, keyStr.slice("defaults.".length), value);
2225
+ (0, import_utils12.set)(proposed, keyStr.slice("defaults.".length), value);
1756
2226
  assertPublicDefaults(proposed);
1757
2227
  }
1758
2228
  this.defaultModelOptions.set(key, value);
@@ -1781,7 +2251,7 @@ var AccessRuntime = class {
1781
2251
  }).onchange("basePath", function(newval, key, target) {
1782
2252
  target[key] = normalizeBasePath(
1783
2253
  modelName,
1784
- (0, import_utils10.isString)(newval) || (0, import_utils10.isNil)(newval) ? newval : void 0
2254
+ (0, import_utils12.isString)(newval) || (0, import_utils12.isNil)(newval) ? newval : void 0
1785
2255
  );
1786
2256
  }).build();
1787
2257
  this.modelJsonSchemas[modelName] = model.jsonSchema();
@@ -1805,13 +2275,14 @@ var AccessRuntime = class {
1805
2275
  setModelOptions(modelName, options) {
1806
2276
  assertPublicDefaults(options.defaults);
1807
2277
  if ("hydration" in options) options = { ...options, hydration: normalizeHydration(options.hydration) };
1808
- const manager = this.getOrCreateModelOptions(modelName);
1809
2278
  const defaultOptions = this.getDefaultModelOptions();
1810
- const currentModelOptions = manager.fetch();
2279
+ const currentModelOptions = this.modelOptions[modelName]?.fetch() ?? { ...defaultModelOptions, modelName };
1811
2280
  const merged = { ...defaultOptions, ...currentModelOptions, ...options };
1812
2281
  const model = this.getModelInstance(modelName);
1813
2282
  const permissionField = merged.documentPermissionField ?? currentModelOptions.documentPermissionField ?? this.getDefaultModelOption("documentPermissionField") ?? "_permissions";
1814
2283
  validateVirtualsTree(model, merged.virtuals, permissionField, modelName);
2284
+ captureVirtualPopulateDependencies(this, modelName, merged);
2285
+ const manager = this.getOrCreateModelOptions(modelName);
1815
2286
  manager.assign({ ...defaultOptions, ...currentModelOptions, ...options });
1816
2287
  }
1817
2288
  setModelOption(modelName, key, value) {
@@ -1824,14 +2295,14 @@ var AccessRuntime = class {
1824
2295
  if (keyStr === "defaults") assertPublicDefaults(value);
1825
2296
  else if (keyStr.startsWith("defaults.")) {
1826
2297
  const proposed = {};
1827
- (0, import_utils10.set)(proposed, keyStr.slice("defaults.".length), value);
2298
+ (0, import_utils12.set)(proposed, keyStr.slice("defaults.".length), value);
1828
2299
  assertPublicDefaults(proposed);
1829
2300
  }
1830
- const manager = this.getOrCreateModelOptions(modelName);
2301
+ const manager = this.modelOptions[modelName];
1831
2302
  if (keyStr === "virtuals" || keyStr.startsWith("virtuals.") || keyStr === "documentPermissionField") {
1832
2303
  const model = this.getModelInstance(modelName);
1833
- const currentVirtuals = manager.get("virtuals");
1834
- const currentPermissionField = manager.get("documentPermissionField") ?? this.getDefaultModelOption("documentPermissionField") ?? "_permissions";
2304
+ const currentVirtuals = manager?.get("virtuals");
2305
+ const currentPermissionField = manager?.get("documentPermissionField") ?? this.getDefaultModelOption("documentPermissionField") ?? "_permissions";
1835
2306
  let proposedVirtuals = currentVirtuals;
1836
2307
  let proposedPermissionField = currentPermissionField;
1837
2308
  if (keyStr === "documentPermissionField") {
@@ -1845,21 +2316,27 @@ var AccessRuntime = class {
1845
2316
  const segments = nestedPath.split(".");
1846
2317
  let cursor = cloned;
1847
2318
  for (let i = 0; i < segments.length - 1; i++) {
1848
- if (!(0, import_utils10.isPlainObject)(cursor)) break;
2319
+ if (!(0, import_utils12.isPlainObject)(cursor)) break;
1849
2320
  cursor = cursor[segments[i]];
1850
2321
  }
1851
- if ((0, import_utils10.isPlainObject)(cursor)) {
2322
+ if ((0, import_utils12.isPlainObject)(cursor)) {
1852
2323
  delete cursor[segments[segments.length - 1]];
1853
2324
  }
1854
2325
  proposedVirtuals = cloned;
1855
2326
  } else {
1856
- (0, import_utils10.set)(cloned, nestedPath, value);
2327
+ (0, import_utils12.set)(cloned, nestedPath, value);
1857
2328
  proposedVirtuals = cloned;
1858
2329
  }
1859
2330
  }
1860
2331
  validateVirtualsTree(model, proposedVirtuals, proposedPermissionField, modelName);
2332
+ captureVirtualPopulateDependencies(this, modelName, {
2333
+ ...this.getDefaultModelOptions(),
2334
+ ...manager?.fetch(),
2335
+ virtuals: proposedVirtuals,
2336
+ documentPermissionField: proposedPermissionField
2337
+ });
1861
2338
  }
1862
- manager.set(key, value);
2339
+ this.getOrCreateModelOptions(modelName).set(key, value);
1863
2340
  }
1864
2341
  /** Frozen stored snapshot, not fully resolved per-path defaults. Read again for current configuration. */
1865
2342
  getModelOptions(modelName) {
@@ -1898,7 +2375,7 @@ var AccessRuntime = class {
1898
2375
  const rootVirtuals = manager.get("virtuals");
1899
2376
  const scopeVirtuals = getScopeVirtualsObject(rootVirtuals, scopePath);
1900
2377
  if (!scopeVirtuals) return [];
1901
- return Object.entries(scopeVirtuals).filter(([, v]) => v !== void 0 && !isVirtualContainerValue(v)).map(([k]) => k);
2378
+ return Object.entries(scopeVirtuals).filter(([, v]) => v !== void 0 && !isVirtualContainerValue2(v)).map(([k]) => k);
1902
2379
  }
1903
2380
  /**
1904
2381
  * Whether a field is a registered virtual leaf in a scope (VIRT-01).
@@ -1912,7 +2389,7 @@ var AccessRuntime = class {
1912
2389
  const scopeVirtuals = getScopeVirtualsObject(rootVirtuals, scopePath);
1913
2390
  if (!scopeVirtuals || !(fieldName in scopeVirtuals)) return false;
1914
2391
  const entry = scopeVirtuals[fieldName];
1915
- if (entry === void 0 || isVirtualContainerValue(entry)) return false;
2392
+ if (entry === void 0 || isVirtualContainerValue2(entry)) return false;
1916
2393
  return true;
1917
2394
  }
1918
2395
  /**
@@ -1922,34 +2399,14 @@ var AccessRuntime = class {
1922
2399
  * interpreted as a getter descriptor when the access is absent: an
1923
2400
  * inapplicable registered name stays virtual (excluded from persisted
1924
2401
  * projections, write admission, and DB sort/filter/distinct).
1925
- * Shared record-level `dependsOn` is inherited when a leaf lacks its own.
2402
+ * Shared `dependsOn` and `dependsOnPopulate` lists inherit independently;
2403
+ * each defined leaf list replaces its shared list wholesale.
1926
2404
  */
1927
2405
  resolveVirtualDescriptor(modelName, virtualName, access, scopePath) {
1928
2406
  const manager = this.getOrCreateModelOptions(modelName);
1929
2407
  const rootVirtuals = manager.get("virtuals");
1930
2408
  const scopeVirtuals = getScopeVirtualsObject(rootVirtuals, scopePath);
1931
- if (!scopeVirtuals || !(virtualName in scopeVirtuals)) return void 0;
1932
- const entry = scopeVirtuals[virtualName];
1933
- if (entry === void 0 || isVirtualContainerValue(entry)) return void 0;
1934
- if ((0, import_utils10.isFunction)(entry)) return { get: entry, dependsOn: [] };
1935
- if (!(0, import_utils10.isPlainObject)(entry)) return void 0;
1936
- const record = entry;
1937
- if ("get" in record) {
1938
- const descriptor = toVirtualDescriptor(entry, void 0);
1939
- return descriptor ?? void 0;
1940
- }
1941
- const sharedDependsOn = record.dependsOn;
1942
- const exact = record[access];
1943
- if (exact !== void 0) {
1944
- const descriptor = toVirtualDescriptor(exact, sharedDependsOn);
1945
- if (descriptor) return descriptor;
1946
- }
1947
- const fallback = record.default;
1948
- if (fallback !== void 0) {
1949
- const descriptor = toVirtualDescriptor(fallback, sharedDependsOn);
1950
- if (descriptor) return descriptor;
1951
- }
1952
- return void 0;
2409
+ return resolveSnapshotDescriptor(scopeVirtuals, virtualName, access);
1953
2410
  }
1954
2411
  getModelNames() {
1955
2412
  return Object.keys(this.modelOptions);
@@ -1968,7 +2425,7 @@ var AccessRuntime = class {
1968
2425
  manager.onchange("basePath", function(newval, key, target) {
1969
2426
  target[key] = normalizeBasePath(
1970
2427
  dataName,
1971
- (0, import_utils10.isString)(newval) || (0, import_utils10.isNil)(newval) ? newval : void 0
2428
+ (0, import_utils12.isString)(newval) || (0, import_utils12.isNil)(newval) ? newval : void 0
1972
2429
  );
1973
2430
  }).build();
1974
2431
  return manager;
@@ -2028,20 +2485,20 @@ var AccessRuntime = class {
2028
2485
  const schema = model.schema;
2029
2486
  this.modelRefs[modelName] = buildRefs(schema.tree);
2030
2487
  this.modelSubs[modelName] = buildSubPaths(schema.tree);
2031
- this.modelAtts[modelName] = (0, import_utils10.keys)(schema.obj);
2488
+ this.modelAtts[modelName] = (0, import_utils12.keys)(schema.obj);
2032
2489
  }
2033
2490
  getModelRef(modelName, refPath) {
2034
2491
  this.ensureModelMeta(modelName);
2035
- const value = (0, import_utils10.get)(this.modelRefs, `${modelName}.${refPath}`, null);
2036
- return (0, import_utils10.isString)(value) ? value : null;
2492
+ const value = (0, import_utils12.get)(this.modelRefs, `${modelName}.${refPath}`, null);
2493
+ return (0, import_utils12.isString)(value) ? value : null;
2037
2494
  }
2038
2495
  getModelSub(modelName) {
2039
2496
  this.ensureModelMeta(modelName);
2040
- return (0, import_utils10.get)(this.modelSubs, modelName, []);
2497
+ return (0, import_utils12.get)(this.modelSubs, modelName, []);
2041
2498
  }
2042
2499
  getModelAtt(modelName) {
2043
2500
  this.ensureModelMeta(modelName);
2044
- return (0, import_utils10.get)(this.modelAtts, modelName, []);
2501
+ return (0, import_utils12.get)(this.modelAtts, modelName, []);
2045
2502
  }
2046
2503
  };
2047
2504
  var defaultRuntime = new AccessRuntime({ allowGlobalModelLookup: true });
@@ -2061,7 +2518,7 @@ var getGlobalOption = (key, defaultValue) => {
2061
2518
 
2062
2519
  // src/validation/common.ts
2063
2520
  var import_zod2 = require("zod");
2064
- var import_utils12 = require("@web-ts-toolkit/utils");
2521
+ var import_utils14 = require("@web-ts-toolkit/utils");
2065
2522
 
2066
2523
  // src/validation/public-admission-schema.ts
2067
2524
  var admissions = /* @__PURE__ */ new WeakMap();
@@ -2083,7 +2540,7 @@ function assertTransformedPublicAdmission(schema, value) {
2083
2540
  }
2084
2541
 
2085
2542
  // src/correlated-includes.ts
2086
- var import_utils11 = require("@web-ts-toolkit/utils");
2543
+ var import_utils13 = require("@web-ts-toolkit/utils");
2087
2544
  var PARENT_MARKER_KEY = "$parent";
2088
2545
  var ESCAPE_WRAPPER_KEY = "$escape";
2089
2546
  var SUBQUERY_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["$$sq", "$$date"]);
@@ -2094,10 +2551,10 @@ var FIELD_SEGMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
2094
2551
  var ARRAY_INDEX_SEGMENT_PATTERN = /^(0|[1-9]\d*)$/;
2095
2552
  var ownKeys = (value) => Object.keys(value);
2096
2553
  function isParentRefMarker(value) {
2097
- return (0, import_utils11.isPlainObject)(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === PARENT_MARKER_KEY;
2554
+ return (0, import_utils13.isPlainObject)(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === PARENT_MARKER_KEY;
2098
2555
  }
2099
2556
  function isEscapedParentRef(value) {
2100
- return (0, import_utils11.isPlainObject)(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === ESCAPE_WRAPPER_KEY;
2557
+ return (0, import_utils13.isPlainObject)(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === ESCAPE_WRAPPER_KEY;
2101
2558
  }
2102
2559
  function hasMarkerKey(value) {
2103
2560
  return ownKeys(value).includes(PARENT_MARKER_KEY) || ownKeys(value).includes(ESCAPE_WRAPPER_KEY);
@@ -2116,12 +2573,12 @@ function containsParentMarkerShape(value) {
2116
2573
  const seen = /* @__PURE__ */ new Set();
2117
2574
  const visit = (current) => {
2118
2575
  if (isParentRefMarker(current) || isEscapedParentRef(current)) return true;
2119
- if ((0, import_utils11.isArray)(current)) {
2576
+ if ((0, import_utils13.isArray)(current)) {
2120
2577
  if (seen.has(current)) return false;
2121
2578
  seen.add(current);
2122
2579
  return current.some(visit);
2123
2580
  }
2124
- if ((0, import_utils11.isPlainObject)(current)) {
2581
+ if ((0, import_utils13.isPlainObject)(current)) {
2125
2582
  if (seen.has(current)) return false;
2126
2583
  seen.add(current);
2127
2584
  if (hasMarkerKey(current)) return true;
@@ -2151,7 +2608,7 @@ function scanTemplateValueNode(value, path, issues) {
2151
2608
  }
2152
2609
  return;
2153
2610
  }
2154
- if ((0, import_utils11.isPlainObject)(value)) {
2611
+ if ((0, import_utils13.isPlainObject)(value)) {
2155
2612
  if (hasMarkerKey(value)) {
2156
2613
  issues.push({
2157
2614
  message: 'Malformed $parent reference: objects carrying $parent must be exactly { "$parent": "<field>" }',
@@ -2162,7 +2619,7 @@ function scanTemplateValueNode(value, path, issues) {
2162
2619
  scanTemplateObjectEntries(value, path, issues);
2163
2620
  return;
2164
2621
  }
2165
- if ((0, import_utils11.isArray)(value)) {
2622
+ if ((0, import_utils13.isArray)(value)) {
2166
2623
  value.forEach((item, index) => scanTemplateValueNode(item, [...path, index], issues));
2167
2624
  }
2168
2625
  }
@@ -2203,7 +2660,7 @@ function scanTemplateObjectEntries(obj, basePath, issues) {
2203
2660
  continue;
2204
2661
  }
2205
2662
  if (LOGICAL_CLAUSE_KEYS.has(key)) {
2206
- if (!(0, import_utils11.isArray)(child)) {
2663
+ if (!(0, import_utils13.isArray)(child)) {
2207
2664
  scanTemplateValueNode(child, childPath, issues);
2208
2665
  continue;
2209
2666
  }
@@ -2216,7 +2673,7 @@ function scanTemplateObjectEntries(obj, basePath, issues) {
2216
2673
  });
2217
2674
  return;
2218
2675
  }
2219
- if ((0, import_utils11.isPlainObject)(clause)) {
2676
+ if ((0, import_utils13.isPlainObject)(clause)) {
2220
2677
  scanTemplateObjectEntries(clause, clausePath, issues);
2221
2678
  }
2222
2679
  });
@@ -2227,7 +2684,7 @@ function scanTemplateObjectEntries(obj, basePath, issues) {
2227
2684
  }
2228
2685
  function validateCorrelatedFilterTemplate(filter) {
2229
2686
  const issues = [];
2230
- if (!(0, import_utils11.isPlainObject)(filter)) {
2687
+ if (!(0, import_utils13.isPlainObject)(filter)) {
2231
2688
  issues.push({ message: "Correlated include filter must be an object", path: [] });
2232
2689
  return issues;
2233
2690
  }
@@ -2355,7 +2812,7 @@ var correlatedFilterSchema = import_zod2.z.record(import_zod2.z.string(), import
2355
2812
  }
2356
2813
  });
2357
2814
  var correlatedEmptyOptionsSchema = import_zod2.z.unknown().refine(
2358
- (value) => value === void 0 || (0, import_utils12.isPlainObject)(value) && Object.keys(value).length === 0,
2815
+ (value) => value === void 0 || (0, import_utils14.isPlainObject)(value) && Object.keys(value).length === 0,
2359
2816
  "Correlated include options must be absent or empty"
2360
2817
  ).optional();
2361
2818
  var correlatedPathSchema = import_zod2.z.string().min(1).superRefine((path, ctx) => {
@@ -2392,7 +2849,7 @@ var correlatedListArgsSchema = import_zod2.z.object({
2392
2849
  include: import_zod2.z.lazy(() => correlatedNestedIncludeSchema).optional()
2393
2850
  }).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
2394
2851
  var correlatedCountArgsSchema = import_zod2.z.unknown().refine(
2395
- (value) => value === void 0 || (0, import_utils12.isPlainObject)(value) && Object.keys(value).length === 0,
2852
+ (value) => value === void 0 || (0, import_utils14.isPlainObject)(value) && Object.keys(value).length === 0,
2396
2853
  "Count includes accept no args"
2397
2854
  ).optional();
2398
2855
  var correlatedReadByIdSchema = import_zod2.z.object({
@@ -2445,7 +2902,7 @@ var correlatedNestedIncludeSchema = import_zod2.z.union([includeItemSchema, impo
2445
2902
  var rejectDuplicateCorrelatedPaths = (items, ctx) => {
2446
2903
  const seen = /* @__PURE__ */ new Set();
2447
2904
  items.forEach((item, index) => {
2448
- if ((0, import_utils12.isPlainObject)(item) && item.mode === "correlated" && typeof item.path === "string") {
2905
+ if ((0, import_utils14.isPlainObject)(item) && item.mode === "correlated" && typeof item.path === "string") {
2449
2906
  if (seen.has(item.path)) {
2450
2907
  ctx.addIssue({
2451
2908
  code: import_zod2.z.ZodIssueCode.custom,
@@ -2483,7 +2940,7 @@ function rejectKeys(body, ctx, keys2) {
2483
2940
  }
2484
2941
 
2485
2942
  // src/validation/parsers.ts
2486
- var clientErrors = import_express_json_router2.default.clientErrors;
2943
+ var clientErrors = import_express_json_router3.default.clientErrors;
2487
2944
  function parsePathParam(value, parameter) {
2488
2945
  const result = stringOrStringArray.safeParse(value);
2489
2946
  if (!result.success) {
@@ -2915,8 +3372,8 @@ function isArkTypeErrors(value) {
2915
3372
  if (typeof value !== "object" || value === null) {
2916
3373
  return false;
2917
3374
  }
2918
- const record = value;
2919
- return record.arkKind === "errors" || record[" arkKind"] === "errors";
3375
+ const record2 = value;
3376
+ return record2.arkKind === "errors" || record2[" arkKind"] === "errors";
2920
3377
  }
2921
3378
  function isThenable(value) {
2922
3379
  return (typeof value === "object" || typeof value === "function") && value !== null && "then" in value && typeof value.then === "function";