@web-ts-toolkit/access-router 0.55.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/index.js CHANGED
@@ -2251,7 +2251,8 @@ function normalizeSelection(select, catalog, label) {
2251
2251
  includeId: selection.includes("_id")
2252
2252
  };
2253
2253
  }
2254
- function captureVirtualPopulateDependencies(runtime, modelName, options) {
2254
+ function captureVirtualPopulateDependencies(runtime, modelName, options, globalOptions = runtime.getGlobalOptions()) {
2255
+ const requireReadOperationAccessForPopulateDependencies = globalOptions.requireReadOperationAccessForPopulateDependencies !== false;
2255
2256
  const model = runtime.getModelInstance(modelName);
2256
2257
  const targets = /* @__PURE__ */ new Map();
2257
2258
  const scopes = [];
@@ -2300,6 +2301,7 @@ function captureVirtualPopulateDependencies(runtime, modelName, options) {
2300
2301
  {
2301
2302
  catalog,
2302
2303
  documentPermissionField,
2304
+ requireReadOperationAccessForPopulateDependencies,
2303
2305
  operationAccess: { read: readOption("operationAccess.read") },
2304
2306
  baseFilter: { read: readOption("baseFilter.read") },
2305
2307
  overrideFilter: { read: readOption("overrideFilter.read") },
@@ -2379,8 +2381,8 @@ function captureVirtualPopulateDependencies(runtime, modelName, options) {
2379
2381
  visit(options.virtuals ?? {}, model?.schema, "", [], !!model && varyingModel(model));
2380
2382
  return Object.freeze({ scopes: Object.freeze(scopes) });
2381
2383
  }
2382
- function captureVirtualPopulateSnapshot(runtime, modelName, options) {
2383
- const snapshot = captureVirtualPopulateDependencies(runtime, modelName, options);
2384
+ function captureVirtualPopulateSnapshot(runtime, modelName, options, globalOptions = runtime.getGlobalOptions()) {
2385
+ const snapshot = captureVirtualPopulateDependencies(runtime, modelName, options, globalOptions);
2384
2386
  const token = () => {
2385
2387
  };
2386
2388
  snapshots.set(token, snapshot);
@@ -2791,6 +2793,7 @@ var init_runtime = __esm({
2791
2793
  requestPermissionField: "_permissions",
2792
2794
  globalPermissions: () => ({}),
2793
2795
  requireRegisteredPopulateModels: true,
2796
+ requireReadOperationAccessForPopulateDependencies: true,
2794
2797
  logger: defaultLogger,
2795
2798
  requestComplexity: defaultRequestComplexity
2796
2799
  },
@@ -2963,9 +2966,16 @@ var init_runtime = __esm({
2963
2966
  }
2964
2967
  /** Shallowly replace supplied global values; nested objects such as requestComplexity are not deep-merged. */
2965
2968
  setGlobalOptions(options) {
2966
- this.globalOptions.assign(options);
2969
+ const next = { ...options };
2970
+ if ("requireReadOperationAccessForPopulateDependencies" in next)
2971
+ next.requireReadOperationAccessForPopulateDependencies = next.requireReadOperationAccessForPopulateDependencies !== false;
2972
+ this.globalOptions.assign(next);
2967
2973
  }
2968
2974
  setGlobalOption(key, value) {
2975
+ if (key === "requireReadOperationAccessForPopulateDependencies") {
2976
+ this.globalOptions.set("requireReadOperationAccessForPopulateDependencies", value !== false);
2977
+ return;
2978
+ }
2969
2979
  this.globalOptions.set(key, value);
2970
2980
  }
2971
2981
  getGlobalOptions() {
@@ -4606,14 +4616,21 @@ function getCapturedAuthorizationOption(configuration, key) {
4606
4616
  const defaultValue = key.startsWith("operationAccess.") ? (0, import_utils21.get)(configuration.operationAccessDefaults, key.slice("operationAccess.".length)) : void 0;
4607
4617
  return resolveNestedOption((path, fallback) => (0, import_utils21.get)(configuration, path, fallback), key, defaultValue);
4608
4618
  }
4609
- function captureModelAuthorizationConfiguration(runtime, modelName, options = runtime.getModelOptions(modelName)) {
4619
+ function captureModelAuthorizationConfiguration(runtime, modelName, options = runtime.getModelOptions(modelName), globalOptions = runtime.getGlobalOptions()) {
4610
4620
  const model = runtime.getModelInstance(modelName);
4611
4621
  if (!model) return authorizationError(`unknown receiving model: ${modelName}`);
4612
4622
  const virtuals = options.virtuals;
4613
4623
  return copyAuthorizationValue(
4614
4624
  {
4615
4625
  catalog: captureAuthorizationFieldCatalog(model.schema, virtuals, options.documentPermissionField),
4616
- virtualPopulateSnapshot: captureVirtualPopulateSnapshot(runtime, modelName, options),
4626
+ virtualPopulateSnapshot: captureVirtualPopulateSnapshot(
4627
+ runtime,
4628
+ modelName,
4629
+ options,
4630
+ globalOptions
4631
+ ),
4632
+ requireReadOperationAccessForPopulateDependencies: globalOptions.requireReadOperationAccessForPopulateDependencies !== false,
4633
+ requireRegisteredPopulateModels: globalOptions.requireRegisteredPopulateModels,
4617
4634
  permissionSchema: options.permissionSchema,
4618
4635
  virtuals,
4619
4636
  alwaysSelectFields: options.alwaysSelectFields,
@@ -5273,7 +5290,8 @@ function createVirtualPopulateBatch(owner) {
5273
5290
  owner: { ...owner },
5274
5291
  rows: [],
5275
5292
  requests: [],
5276
- rowCache: owner.core && getRequestRowCache(owner.core) || new Cache()
5293
+ rowCache: owner.core && getRequestRowCache(owner.core) || new Cache(),
5294
+ readGuards: /* @__PURE__ */ new Map()
5277
5295
  });
5278
5296
  return batch;
5279
5297
  }
@@ -5322,12 +5340,19 @@ function captureVirtualPopulateRows(batch, inputs, plan, publicPopulate = [], so
5322
5340
  }
5323
5341
  const record3 = storage(value);
5324
5342
  if (!record3) return;
5325
- const occurrence = { address, plan: currentPlan, refs: /* @__PURE__ */ new Map(), dependencies: /* @__PURE__ */ new Map() };
5343
+ const occurrence = {
5344
+ address,
5345
+ plan: currentPlan,
5346
+ refs: /* @__PURE__ */ new Map(),
5347
+ referenceTargets: /* @__PURE__ */ new Map(),
5348
+ dependencies: /* @__PURE__ */ new Map()
5349
+ };
5326
5350
  row.occurrences.set(addressKey(address), occurrence);
5327
5351
  for (const candidate of currentPlan.candidates) {
5328
5352
  const dependencies = (candidate.populateDependencies ?? []).map((dependency) => {
5329
5353
  if (!occurrence.refs.has(dependency.path))
5330
5354
  occurrence.refs.set(dependency.path, originalRef(value, dependency.path));
5355
+ occurrence.referenceTargets.set(dependency.path, dependency.target);
5331
5356
  const request = {
5332
5357
  binding: dependency.target,
5333
5358
  target: dependency.target,
@@ -5388,6 +5413,27 @@ function referenceIds(original) {
5388
5413
  if (!original.present || original.value == null) return [];
5389
5414
  return (Array.isArray(original.value) ? original.value : [original.value]).filter((id) => id != null);
5390
5415
  }
5416
+ function targetReadAllowed(state, target) {
5417
+ let pending = state.readGuards.get(target.identity);
5418
+ if (!pending) {
5419
+ const { owner } = state;
5420
+ pending = getRequestWorkState(owner.request, owner.runtime).hookGate.run(
5421
+ () => evaluateRouteGuard(
5422
+ owner.request,
5423
+ owner.permissions,
5424
+ getCapturedAuthorizationOption(target.configuration, "operationAccess.read")
5425
+ )
5426
+ ).then(
5427
+ (allowed) => !!allowed,
5428
+ () => {
5429
+ warn("Virtual populated dependency unavailable", { modelName: target.modelName, category: "policy" });
5430
+ return false;
5431
+ }
5432
+ );
5433
+ state.readGuards.set(target.identity, pending);
5434
+ }
5435
+ return pending;
5436
+ }
5391
5437
  function executeVirtualPopulateBatch(batch) {
5392
5438
  const state = batches.get(batch);
5393
5439
  if (!state) throw new Error("Unknown private populate batch");
@@ -5404,14 +5450,8 @@ async function execute(state) {
5404
5450
  if (!pending) {
5405
5451
  pending = (async () => {
5406
5452
  try {
5407
- const allowed = await work.hookGate.run(
5408
- () => evaluateRouteGuard(
5409
- owner.request,
5410
- owner.permissions,
5411
- getCapturedAuthorizationOption(target.configuration, "operationAccess.read")
5412
- )
5413
- );
5414
- if (!allowed) return void 0;
5453
+ if (target.configuration.requireReadOperationAccessForPopulateDependencies !== false && !await targetReadAllowed(state, target))
5454
+ return void 0;
5415
5455
  const parts = await work.hookGate.run(
5416
5456
  () => resolveAccessFilterParts({
5417
5457
  req: owner.request,
@@ -5496,6 +5536,12 @@ function readVirtualPopulateOriginalReference(row, address, path) {
5496
5536
  function hasVirtualPopulatePublicReference(row, address, path) {
5497
5537
  return !!rows.get(row)?.publicRequests.some((request) => addressKey(request.address) === addressKey([...address, path]));
5498
5538
  }
5539
+ function isVirtualPopulateReferenceVisible(row, address, path) {
5540
+ const record3 = rows.get(row);
5541
+ const occurrence = record3?.occurrences.get(addressKey(address));
5542
+ const target = occurrence?.refs.get(path)?.present ? occurrence.referenceTargets.get(path) : void 0;
5543
+ return record3 && target ? targetReadAllowed(record3.batch, target) : Promise.resolve(true);
5544
+ }
5499
5545
  function readVirtualPopulateInputs(row, address, virtualName) {
5500
5546
  const record3 = rows.get(row);
5501
5547
  if (!record3?.batch.completed) return { available: false };
@@ -5654,7 +5700,7 @@ var init_virtual_populate_execution = __esm({
5654
5700
  const option = getCapturedAuthorizationOption(configuration, `${key}.${access}`);
5655
5701
  return key === "operationAccess" || typeof option === "function" ? option : void 0;
5656
5702
  });
5657
- compatible = (a, b) => a.binding.runtime === b.binding.runtime && a.binding.model === b.binding.model && a.binding.schema === b.binding.schema && a.access === b.access && a.requestRead === b.requestRead && // Different captures may share only an equivalent terminal persisted binding/policy.
5703
+ compatible = (a, b) => a.binding.runtime === b.binding.runtime && a.binding.model === b.binding.model && a.binding.schema === b.binding.schema && a.access === b.access && a.requestRead === b.requestRead && a.binding.configuration.requireReadOperationAccessForPopulateDependencies === b.binding.configuration.requireReadOperationAccessForPopulateDependencies && // Different captures may share only an equivalent terminal persisted binding/policy.
5658
5704
  equivalentVirtualPopulatePredicate(
5659
5705
  a.binding.configuration.catalog.persistedPaths,
5660
5706
  b.binding.configuration.catalog.persistedPaths
@@ -6364,6 +6410,7 @@ async function finalizeSingleDoc(args, input) {
6364
6410
  if (!captured && documentPermissionField in output) keep.add(documentPermissionField);
6365
6411
  const trimmed = {};
6366
6412
  for (const key of keep) {
6413
+ if (privateRow && !await isVirtualPopulateReferenceVisible(privateRow, privateAddress, key)) continue;
6367
6414
  if (privateRow && !hasVirtualPopulatePublicReference(privateRow, privateAddress, key)) {
6368
6415
  const ref = readVirtualPopulateOriginalReference(privateRow, privateAddress, key);
6369
6416
  if (ref) {
@@ -7269,6 +7316,11 @@ function modelServiceOptions(receiver, read) {
7269
7316
  if (call?.receiver !== receiver) return read();
7270
7317
  return call.options ??= read();
7271
7318
  }
7319
+ function modelServiceGlobalOptions(receiver, runtime) {
7320
+ const call = dispatch.getStore();
7321
+ if (call?.receiver !== receiver) return runtime.getGlobalOptions();
7322
+ return call.globalOptions ??= runtime.getGlobalOptions();
7323
+ }
7272
7324
 
7273
7325
  // src/services/base.ts
7274
7326
  init_request_work();
@@ -7709,7 +7761,7 @@ function staticObjectIdReference(runtime, source, path, target) {
7709
7761
  const scalar = type?.instance === "Array" ? type.$embeddedSchemaType ?? type.embeddedSchemaType ?? type.caster : type;
7710
7762
  return scalar?.instance === "ObjectId" && (type?.options?.ref ?? scalar.options?.ref) === target && type?.options?.refPath === void 0 && scalar.options?.refPath === void 0;
7711
7763
  }
7712
- function captureRelatedTargets(runtime, modelName, args = {}, depth = 0) {
7764
+ function captureRelatedTargets(runtime, modelName, args = {}, depth = 0, globalOptions = runtime.getGlobalOptions()) {
7713
7765
  if (depth > 32) authorizationError("related descriptor depth exceeded");
7714
7766
  assertPublicAdmission(args.populate, "populate");
7715
7767
  assertPublicAdmission(args.include, "include");
@@ -7723,16 +7775,16 @@ function captureRelatedTargets(runtime, modelName, args = {}, depth = 0) {
7723
7775
  targets.push({
7724
7776
  path,
7725
7777
  modelName: model,
7726
- configuration: captureModelAuthorizationConfiguration(runtime, model),
7778
+ configuration: captureModelAuthorizationConfiguration(runtime, model, void 0, globalOptions),
7727
7779
  staticObjectIdReference: staticObjectIdReference(runtime, modelName, path, model)
7728
7780
  });
7729
- else if (model && !runtime.getGlobalOption("requireRegisteredPopulateModels", true)) {
7781
+ else if (model && !(globalOptions.requireRegisteredPopulateModels === void 0 ? true : globalOptions.requireRegisteredPopulateModels)) {
7730
7782
  targets.push({ path, modelName: model, kind: "passthrough", configuration: { catalog: { paths: [] } } });
7731
7783
  }
7732
7784
  }
7733
7785
  for (const entry of entries(args.include)) {
7734
7786
  if (!runtime.hasModel(entry.model)) authorizationError(`Model ${entry.model} not found`);
7735
- const configuration = captureModelAuthorizationConfiguration(runtime, entry.model);
7787
+ const configuration = captureModelAuthorizationConfiguration(runtime, entry.model, void 0, globalOptions);
7736
7788
  const queryIntent = entry.op === "count" ? void 0 : captureModelServiceIntent(
7737
7789
  {
7738
7790
  defaults: configuration.defaults ?? {},
@@ -7758,7 +7810,13 @@ function captureRelatedTargets(runtime, modelName, args = {}, depth = 0) {
7758
7810
  configuration,
7759
7811
  queryIntent,
7760
7812
  operationGuard: runtime.getModelOption(entry.model, `operationAccess.${entry.op}`),
7761
- relatedTargets: captureRelatedTargets(runtime, entry.model, queryIntent?.args ?? entry.args ?? {}, depth + 1)
7813
+ relatedTargets: captureRelatedTargets(
7814
+ runtime,
7815
+ entry.model,
7816
+ queryIntent?.args ?? entry.args ?? {},
7817
+ depth + 1,
7818
+ globalOptions
7819
+ )
7762
7820
  });
7763
7821
  }
7764
7822
  if (new Set(targets.map((target) => target.path)).size !== targets.length)
@@ -9330,7 +9388,9 @@ function prepareSubdocumentOutput(service, sub, operation, virtualAccess, output
9330
9388
  captureRelatedTargets(
9331
9389
  getServiceExecutors(service).binding().runtime,
9332
9390
  service.getServiceModelName(),
9333
- { populate }
9391
+ { populate },
9392
+ 0,
9393
+ configuration
9334
9394
  ),
9335
9395
  void 0,
9336
9396
  configuration,
@@ -10369,10 +10429,12 @@ var Service = class extends Base {
10369
10429
  model: this.model.mongooseModel,
10370
10430
  modelName: this.modelName
10371
10431
  }),
10372
- configuration: () => this.requestRuntime.getModelInstance(this.modelName) ? captureModelAuthorizationConfiguration(this.requestRuntime, this.modelName, {
10373
- ...this.options,
10374
- defaults: this.defaults
10375
- }) : { catalog: { paths: [] }, ...this.options },
10432
+ configuration: () => this.requestRuntime.getModelInstance(this.modelName) ? captureModelAuthorizationConfiguration(
10433
+ this.requestRuntime,
10434
+ this.modelName,
10435
+ { ...this.options, defaults: this.defaults },
10436
+ modelServiceGlobalOptions(this, this.requestRuntime)
10437
+ ) : { catalog: { paths: [] }, ...this.options },
10376
10438
  permissions: () => this.requestCore.getPermissions?.() ?? new permission_default({}),
10377
10439
  parse: (filter2) => this.parseClientData(filter2, this.getCorrelatedExecState().scheduler),
10378
10440
  validate: (filter2, expanded) => {
@@ -10901,12 +10963,13 @@ var Service = class extends Base {
10901
10963
  });
10902
10964
  }
10903
10965
  captureRelated(args, defaults) {
10966
+ const globalOptions = modelServiceGlobalOptions(this, this.requestRuntime);
10904
10967
  const effective = { populate: args?.populate ?? defaults?.populate, include: args?.include ?? defaults?.include };
10905
10968
  if (effective.include) {
10906
10969
  const processed = this.processInclude(effective.include);
10907
10970
  effective.include = [...processed.includes, ...processed.correlatedIncludes];
10908
10971
  }
10909
- return captureRelatedTargets(this.requestRuntime, this.modelName, effective);
10972
+ return captureRelatedTargets(this.requestRuntime, this.modelName, effective, 0, globalOptions);
10910
10973
  }
10911
10974
  /** Canonical population uses the target's retained policy for both fetch and output. */
10912
10975
  async genExecutionPopulate(execution, access, populate) {
@@ -10928,12 +10991,11 @@ var Service = class extends Base {
10928
10991
  modelPermissionPrefix: captured.configuration.modelPermissionPrefix
10929
10992
  });
10930
10993
  if (sourceState === "denied") continue;
10931
- const modelName = getModelRef(this.modelName, descriptor.path);
10994
+ const target = captured.relatedTargets.find((target2) => target2.path === descriptor.path);
10995
+ const modelName = target?.modelName;
10932
10996
  if (!modelName) continue;
10933
- if (!this.requestRuntime.hasModel(modelName)) {
10934
- if (captured.relatedTargets.some((target) => target.path === descriptor.path && target.kind === "passthrough")) {
10935
- result2.push({ path: descriptor.path, select: descriptor.select });
10936
- }
10997
+ if (target.kind === "passthrough") {
10998
+ result2.push({ path: descriptor.path, select: descriptor.select });
10937
10999
  continue;
10938
11000
  }
10939
11001
  const policy = relatedPolicy(state.authorization, state.binding, descriptor.path);
@@ -10986,7 +11048,7 @@ var Service = class extends Base {
10986
11048
  schema: childState.binding.model.schema,
10987
11049
  configuration: targetConfiguration,
10988
11050
  requestRead: targetAccess === "read" && policy?.operation.mode !== "grant" && readServiceAuthorization(childState.authorization, childState.binding).authorization.rows.mode === "request",
10989
- staticObjectIdReference: captured.relatedTargets.find((target) => target.path === descriptor.path)?.staticObjectIdReference
11051
+ staticObjectIdReference: captured.relatedTargets.find((target2) => target2.path === descriptor.path)?.staticObjectIdReference
10990
11052
  }
10991
11053
  });
10992
11054
  result2.push(ret);
@@ -11030,15 +11092,13 @@ var Service = class extends Base {
11030
11092
  transferDocumentAuthorizationGraph(source, copy2);
11031
11093
  return copy2;
11032
11094
  }
11033
- let targetMongooseModel;
11034
- try {
11035
- const inst = this.requestRuntime.getModelInstance?.(
11036
- meta.targetModelName
11037
- );
11038
- void inst;
11039
- targetMongooseModel = inst;
11040
- } catch {
11041
- targetMongooseModel = parentContext.mongooseModel;
11095
+ let targetMongooseModel = meta.fetchBinding?.model;
11096
+ if (!targetMongooseModel) {
11097
+ try {
11098
+ targetMongooseModel = this.requestRuntime.getModelInstance(meta.targetModelName);
11099
+ } catch {
11100
+ targetMongooseModel = parentContext.mongooseModel;
11101
+ }
11042
11102
  }
11043
11103
  const targetContext = {
11044
11104
  mongooseModel: targetMongooseModel ?? parentContext.mongooseModel,
package/index.mjs CHANGED
@@ -2231,7 +2231,8 @@ function normalizeSelection(select, catalog, label) {
2231
2231
  includeId: selection.includes("_id")
2232
2232
  };
2233
2233
  }
2234
- function captureVirtualPopulateDependencies(runtime, modelName, options) {
2234
+ function captureVirtualPopulateDependencies(runtime, modelName, options, globalOptions = runtime.getGlobalOptions()) {
2235
+ const requireReadOperationAccessForPopulateDependencies = globalOptions.requireReadOperationAccessForPopulateDependencies !== false;
2235
2236
  const model = runtime.getModelInstance(modelName);
2236
2237
  const targets = /* @__PURE__ */ new Map();
2237
2238
  const scopes = [];
@@ -2280,6 +2281,7 @@ function captureVirtualPopulateDependencies(runtime, modelName, options) {
2280
2281
  {
2281
2282
  catalog,
2282
2283
  documentPermissionField,
2284
+ requireReadOperationAccessForPopulateDependencies,
2283
2285
  operationAccess: { read: readOption("operationAccess.read") },
2284
2286
  baseFilter: { read: readOption("baseFilter.read") },
2285
2287
  overrideFilter: { read: readOption("overrideFilter.read") },
@@ -2359,8 +2361,8 @@ function captureVirtualPopulateDependencies(runtime, modelName, options) {
2359
2361
  visit(options.virtuals ?? {}, model?.schema, "", [], !!model && varyingModel(model));
2360
2362
  return Object.freeze({ scopes: Object.freeze(scopes) });
2361
2363
  }
2362
- function captureVirtualPopulateSnapshot(runtime, modelName, options) {
2363
- const snapshot = captureVirtualPopulateDependencies(runtime, modelName, options);
2364
+ function captureVirtualPopulateSnapshot(runtime, modelName, options, globalOptions = runtime.getGlobalOptions()) {
2365
+ const snapshot = captureVirtualPopulateDependencies(runtime, modelName, options, globalOptions);
2364
2366
  const token = () => {
2365
2367
  };
2366
2368
  snapshots.set(token, snapshot);
@@ -2783,6 +2785,7 @@ var init_runtime = __esm({
2783
2785
  requestPermissionField: "_permissions",
2784
2786
  globalPermissions: () => ({}),
2785
2787
  requireRegisteredPopulateModels: true,
2788
+ requireReadOperationAccessForPopulateDependencies: true,
2786
2789
  logger: defaultLogger,
2787
2790
  requestComplexity: defaultRequestComplexity
2788
2791
  },
@@ -2955,9 +2958,16 @@ var init_runtime = __esm({
2955
2958
  }
2956
2959
  /** Shallowly replace supplied global values; nested objects such as requestComplexity are not deep-merged. */
2957
2960
  setGlobalOptions(options) {
2958
- this.globalOptions.assign(options);
2961
+ const next = { ...options };
2962
+ if ("requireReadOperationAccessForPopulateDependencies" in next)
2963
+ next.requireReadOperationAccessForPopulateDependencies = next.requireReadOperationAccessForPopulateDependencies !== false;
2964
+ this.globalOptions.assign(next);
2959
2965
  }
2960
2966
  setGlobalOption(key, value) {
2967
+ if (key === "requireReadOperationAccessForPopulateDependencies") {
2968
+ this.globalOptions.set("requireReadOperationAccessForPopulateDependencies", value !== false);
2969
+ return;
2970
+ }
2961
2971
  this.globalOptions.set(key, value);
2962
2972
  }
2963
2973
  getGlobalOptions() {
@@ -4599,14 +4609,21 @@ function getCapturedAuthorizationOption(configuration, key) {
4599
4609
  const defaultValue = key.startsWith("operationAccess.") ? get7(configuration.operationAccessDefaults, key.slice("operationAccess.".length)) : void 0;
4600
4610
  return resolveNestedOption((path, fallback) => get7(configuration, path, fallback), key, defaultValue);
4601
4611
  }
4602
- function captureModelAuthorizationConfiguration(runtime, modelName, options = runtime.getModelOptions(modelName)) {
4612
+ function captureModelAuthorizationConfiguration(runtime, modelName, options = runtime.getModelOptions(modelName), globalOptions = runtime.getGlobalOptions()) {
4603
4613
  const model = runtime.getModelInstance(modelName);
4604
4614
  if (!model) return authorizationError(`unknown receiving model: ${modelName}`);
4605
4615
  const virtuals = options.virtuals;
4606
4616
  return copyAuthorizationValue(
4607
4617
  {
4608
4618
  catalog: captureAuthorizationFieldCatalog(model.schema, virtuals, options.documentPermissionField),
4609
- virtualPopulateSnapshot: captureVirtualPopulateSnapshot(runtime, modelName, options),
4619
+ virtualPopulateSnapshot: captureVirtualPopulateSnapshot(
4620
+ runtime,
4621
+ modelName,
4622
+ options,
4623
+ globalOptions
4624
+ ),
4625
+ requireReadOperationAccessForPopulateDependencies: globalOptions.requireReadOperationAccessForPopulateDependencies !== false,
4626
+ requireRegisteredPopulateModels: globalOptions.requireRegisteredPopulateModels,
4610
4627
  permissionSchema: options.permissionSchema,
4611
4628
  virtuals,
4612
4629
  alwaysSelectFields: options.alwaysSelectFields,
@@ -5277,7 +5294,8 @@ function createVirtualPopulateBatch(owner) {
5277
5294
  owner: { ...owner },
5278
5295
  rows: [],
5279
5296
  requests: [],
5280
- rowCache: owner.core && getRequestRowCache(owner.core) || new Cache()
5297
+ rowCache: owner.core && getRequestRowCache(owner.core) || new Cache(),
5298
+ readGuards: /* @__PURE__ */ new Map()
5281
5299
  });
5282
5300
  return batch;
5283
5301
  }
@@ -5326,12 +5344,19 @@ function captureVirtualPopulateRows(batch, inputs, plan, publicPopulate = [], so
5326
5344
  }
5327
5345
  const record3 = storage(value);
5328
5346
  if (!record3) return;
5329
- const occurrence = { address, plan: currentPlan, refs: /* @__PURE__ */ new Map(), dependencies: /* @__PURE__ */ new Map() };
5347
+ const occurrence = {
5348
+ address,
5349
+ plan: currentPlan,
5350
+ refs: /* @__PURE__ */ new Map(),
5351
+ referenceTargets: /* @__PURE__ */ new Map(),
5352
+ dependencies: /* @__PURE__ */ new Map()
5353
+ };
5330
5354
  row.occurrences.set(addressKey(address), occurrence);
5331
5355
  for (const candidate of currentPlan.candidates) {
5332
5356
  const dependencies = (candidate.populateDependencies ?? []).map((dependency) => {
5333
5357
  if (!occurrence.refs.has(dependency.path))
5334
5358
  occurrence.refs.set(dependency.path, originalRef(value, dependency.path));
5359
+ occurrence.referenceTargets.set(dependency.path, dependency.target);
5335
5360
  const request = {
5336
5361
  binding: dependency.target,
5337
5362
  target: dependency.target,
@@ -5392,6 +5417,27 @@ function referenceIds(original) {
5392
5417
  if (!original.present || original.value == null) return [];
5393
5418
  return (Array.isArray(original.value) ? original.value : [original.value]).filter((id) => id != null);
5394
5419
  }
5420
+ function targetReadAllowed(state, target) {
5421
+ let pending = state.readGuards.get(target.identity);
5422
+ if (!pending) {
5423
+ const { owner } = state;
5424
+ pending = getRequestWorkState(owner.request, owner.runtime).hookGate.run(
5425
+ () => evaluateRouteGuard(
5426
+ owner.request,
5427
+ owner.permissions,
5428
+ getCapturedAuthorizationOption(target.configuration, "operationAccess.read")
5429
+ )
5430
+ ).then(
5431
+ (allowed) => !!allowed,
5432
+ () => {
5433
+ warn("Virtual populated dependency unavailable", { modelName: target.modelName, category: "policy" });
5434
+ return false;
5435
+ }
5436
+ );
5437
+ state.readGuards.set(target.identity, pending);
5438
+ }
5439
+ return pending;
5440
+ }
5395
5441
  function executeVirtualPopulateBatch(batch) {
5396
5442
  const state = batches.get(batch);
5397
5443
  if (!state) throw new Error("Unknown private populate batch");
@@ -5408,14 +5454,8 @@ async function execute(state) {
5408
5454
  if (!pending) {
5409
5455
  pending = (async () => {
5410
5456
  try {
5411
- const allowed = await work.hookGate.run(
5412
- () => evaluateRouteGuard(
5413
- owner.request,
5414
- owner.permissions,
5415
- getCapturedAuthorizationOption(target.configuration, "operationAccess.read")
5416
- )
5417
- );
5418
- if (!allowed) return void 0;
5457
+ if (target.configuration.requireReadOperationAccessForPopulateDependencies !== false && !await targetReadAllowed(state, target))
5458
+ return void 0;
5419
5459
  const parts = await work.hookGate.run(
5420
5460
  () => resolveAccessFilterParts({
5421
5461
  req: owner.request,
@@ -5500,6 +5540,12 @@ function readVirtualPopulateOriginalReference(row, address, path) {
5500
5540
  function hasVirtualPopulatePublicReference(row, address, path) {
5501
5541
  return !!rows.get(row)?.publicRequests.some((request) => addressKey(request.address) === addressKey([...address, path]));
5502
5542
  }
5543
+ function isVirtualPopulateReferenceVisible(row, address, path) {
5544
+ const record3 = rows.get(row);
5545
+ const occurrence = record3?.occurrences.get(addressKey(address));
5546
+ const target = occurrence?.refs.get(path)?.present ? occurrence.referenceTargets.get(path) : void 0;
5547
+ return record3 && target ? targetReadAllowed(record3.batch, target) : Promise.resolve(true);
5548
+ }
5503
5549
  function readVirtualPopulateInputs(row, address, virtualName) {
5504
5550
  const record3 = rows.get(row);
5505
5551
  if (!record3?.batch.completed) return { available: false };
@@ -5655,7 +5701,7 @@ var init_virtual_populate_execution = __esm({
5655
5701
  const option = getCapturedAuthorizationOption(configuration, `${key}.${access}`);
5656
5702
  return key === "operationAccess" || typeof option === "function" ? option : void 0;
5657
5703
  });
5658
- compatible = (a, b) => a.binding.runtime === b.binding.runtime && a.binding.model === b.binding.model && a.binding.schema === b.binding.schema && a.access === b.access && a.requestRead === b.requestRead && // Different captures may share only an equivalent terminal persisted binding/policy.
5704
+ compatible = (a, b) => a.binding.runtime === b.binding.runtime && a.binding.model === b.binding.model && a.binding.schema === b.binding.schema && a.access === b.access && a.requestRead === b.requestRead && a.binding.configuration.requireReadOperationAccessForPopulateDependencies === b.binding.configuration.requireReadOperationAccessForPopulateDependencies && // Different captures may share only an equivalent terminal persisted binding/policy.
5659
5705
  equivalentVirtualPopulatePredicate(
5660
5706
  a.binding.configuration.catalog.persistedPaths,
5661
5707
  b.binding.configuration.catalog.persistedPaths
@@ -6366,6 +6412,7 @@ async function finalizeSingleDoc(args, input) {
6366
6412
  if (!captured && documentPermissionField in output) keep.add(documentPermissionField);
6367
6413
  const trimmed = {};
6368
6414
  for (const key of keep) {
6415
+ if (privateRow && !await isVirtualPopulateReferenceVisible(privateRow, privateAddress, key)) continue;
6369
6416
  if (privateRow && !hasVirtualPopulatePublicReference(privateRow, privateAddress, key)) {
6370
6417
  const ref = readVirtualPopulateOriginalReference(privateRow, privateAddress, key);
6371
6418
  if (ref) {
@@ -7242,6 +7289,11 @@ function modelServiceOptions(receiver, read) {
7242
7289
  if (call?.receiver !== receiver) return read();
7243
7290
  return call.options ??= read();
7244
7291
  }
7292
+ function modelServiceGlobalOptions(receiver, runtime) {
7293
+ const call = dispatch.getStore();
7294
+ if (call?.receiver !== receiver) return runtime.getGlobalOptions();
7295
+ return call.globalOptions ??= runtime.getGlobalOptions();
7296
+ }
7245
7297
 
7246
7298
  // src/services/base.ts
7247
7299
  init_request_work();
@@ -7682,7 +7734,7 @@ function staticObjectIdReference(runtime, source, path, target) {
7682
7734
  const scalar = type?.instance === "Array" ? type.$embeddedSchemaType ?? type.embeddedSchemaType ?? type.caster : type;
7683
7735
  return scalar?.instance === "ObjectId" && (type?.options?.ref ?? scalar.options?.ref) === target && type?.options?.refPath === void 0 && scalar.options?.refPath === void 0;
7684
7736
  }
7685
- function captureRelatedTargets(runtime, modelName, args = {}, depth = 0) {
7737
+ function captureRelatedTargets(runtime, modelName, args = {}, depth = 0, globalOptions = runtime.getGlobalOptions()) {
7686
7738
  if (depth > 32) authorizationError("related descriptor depth exceeded");
7687
7739
  assertPublicAdmission(args.populate, "populate");
7688
7740
  assertPublicAdmission(args.include, "include");
@@ -7696,16 +7748,16 @@ function captureRelatedTargets(runtime, modelName, args = {}, depth = 0) {
7696
7748
  targets.push({
7697
7749
  path,
7698
7750
  modelName: model,
7699
- configuration: captureModelAuthorizationConfiguration(runtime, model),
7751
+ configuration: captureModelAuthorizationConfiguration(runtime, model, void 0, globalOptions),
7700
7752
  staticObjectIdReference: staticObjectIdReference(runtime, modelName, path, model)
7701
7753
  });
7702
- else if (model && !runtime.getGlobalOption("requireRegisteredPopulateModels", true)) {
7754
+ else if (model && !(globalOptions.requireRegisteredPopulateModels === void 0 ? true : globalOptions.requireRegisteredPopulateModels)) {
7703
7755
  targets.push({ path, modelName: model, kind: "passthrough", configuration: { catalog: { paths: [] } } });
7704
7756
  }
7705
7757
  }
7706
7758
  for (const entry of entries(args.include)) {
7707
7759
  if (!runtime.hasModel(entry.model)) authorizationError(`Model ${entry.model} not found`);
7708
- const configuration = captureModelAuthorizationConfiguration(runtime, entry.model);
7760
+ const configuration = captureModelAuthorizationConfiguration(runtime, entry.model, void 0, globalOptions);
7709
7761
  const queryIntent = entry.op === "count" ? void 0 : captureModelServiceIntent(
7710
7762
  {
7711
7763
  defaults: configuration.defaults ?? {},
@@ -7731,7 +7783,13 @@ function captureRelatedTargets(runtime, modelName, args = {}, depth = 0) {
7731
7783
  configuration,
7732
7784
  queryIntent,
7733
7785
  operationGuard: runtime.getModelOption(entry.model, `operationAccess.${entry.op}`),
7734
- relatedTargets: captureRelatedTargets(runtime, entry.model, queryIntent?.args ?? entry.args ?? {}, depth + 1)
7786
+ relatedTargets: captureRelatedTargets(
7787
+ runtime,
7788
+ entry.model,
7789
+ queryIntent?.args ?? entry.args ?? {},
7790
+ depth + 1,
7791
+ globalOptions
7792
+ )
7735
7793
  });
7736
7794
  }
7737
7795
  if (new Set(targets.map((target) => target.path)).size !== targets.length)
@@ -9315,7 +9373,9 @@ function prepareSubdocumentOutput(service, sub, operation, virtualAccess, output
9315
9373
  captureRelatedTargets(
9316
9374
  getServiceExecutors(service).binding().runtime,
9317
9375
  service.getServiceModelName(),
9318
- { populate }
9376
+ { populate },
9377
+ 0,
9378
+ configuration
9319
9379
  ),
9320
9380
  void 0,
9321
9381
  configuration,
@@ -10354,10 +10414,12 @@ var Service = class extends Base {
10354
10414
  model: this.model.mongooseModel,
10355
10415
  modelName: this.modelName
10356
10416
  }),
10357
- configuration: () => this.requestRuntime.getModelInstance(this.modelName) ? captureModelAuthorizationConfiguration(this.requestRuntime, this.modelName, {
10358
- ...this.options,
10359
- defaults: this.defaults
10360
- }) : { catalog: { paths: [] }, ...this.options },
10417
+ configuration: () => this.requestRuntime.getModelInstance(this.modelName) ? captureModelAuthorizationConfiguration(
10418
+ this.requestRuntime,
10419
+ this.modelName,
10420
+ { ...this.options, defaults: this.defaults },
10421
+ modelServiceGlobalOptions(this, this.requestRuntime)
10422
+ ) : { catalog: { paths: [] }, ...this.options },
10361
10423
  permissions: () => this.requestCore.getPermissions?.() ?? new permission_default({}),
10362
10424
  parse: (filter2) => this.parseClientData(filter2, this.getCorrelatedExecState().scheduler),
10363
10425
  validate: (filter2, expanded) => {
@@ -10886,12 +10948,13 @@ var Service = class extends Base {
10886
10948
  });
10887
10949
  }
10888
10950
  captureRelated(args, defaults) {
10951
+ const globalOptions = modelServiceGlobalOptions(this, this.requestRuntime);
10889
10952
  const effective = { populate: args?.populate ?? defaults?.populate, include: args?.include ?? defaults?.include };
10890
10953
  if (effective.include) {
10891
10954
  const processed = this.processInclude(effective.include);
10892
10955
  effective.include = [...processed.includes, ...processed.correlatedIncludes];
10893
10956
  }
10894
- return captureRelatedTargets(this.requestRuntime, this.modelName, effective);
10957
+ return captureRelatedTargets(this.requestRuntime, this.modelName, effective, 0, globalOptions);
10895
10958
  }
10896
10959
  /** Canonical population uses the target's retained policy for both fetch and output. */
10897
10960
  async genExecutionPopulate(execution, access, populate) {
@@ -10913,12 +10976,11 @@ var Service = class extends Base {
10913
10976
  modelPermissionPrefix: captured.configuration.modelPermissionPrefix
10914
10977
  });
10915
10978
  if (sourceState === "denied") continue;
10916
- const modelName = getModelRef(this.modelName, descriptor.path);
10979
+ const target = captured.relatedTargets.find((target2) => target2.path === descriptor.path);
10980
+ const modelName = target?.modelName;
10917
10981
  if (!modelName) continue;
10918
- if (!this.requestRuntime.hasModel(modelName)) {
10919
- if (captured.relatedTargets.some((target) => target.path === descriptor.path && target.kind === "passthrough")) {
10920
- result2.push({ path: descriptor.path, select: descriptor.select });
10921
- }
10982
+ if (target.kind === "passthrough") {
10983
+ result2.push({ path: descriptor.path, select: descriptor.select });
10922
10984
  continue;
10923
10985
  }
10924
10986
  const policy = relatedPolicy(state.authorization, state.binding, descriptor.path);
@@ -10971,7 +11033,7 @@ var Service = class extends Base {
10971
11033
  schema: childState.binding.model.schema,
10972
11034
  configuration: targetConfiguration,
10973
11035
  requestRead: targetAccess === "read" && policy?.operation.mode !== "grant" && readServiceAuthorization(childState.authorization, childState.binding).authorization.rows.mode === "request",
10974
- staticObjectIdReference: captured.relatedTargets.find((target) => target.path === descriptor.path)?.staticObjectIdReference
11036
+ staticObjectIdReference: captured.relatedTargets.find((target2) => target2.path === descriptor.path)?.staticObjectIdReference
10975
11037
  }
10976
11038
  });
10977
11039
  result2.push(ret);
@@ -11015,15 +11077,13 @@ var Service = class extends Base {
11015
11077
  transferDocumentAuthorizationGraph(source, copy2);
11016
11078
  return copy2;
11017
11079
  }
11018
- let targetMongooseModel;
11019
- try {
11020
- const inst = this.requestRuntime.getModelInstance?.(
11021
- meta.targetModelName
11022
- );
11023
- void inst;
11024
- targetMongooseModel = inst;
11025
- } catch {
11026
- targetMongooseModel = parentContext.mongooseModel;
11080
+ let targetMongooseModel = meta.fetchBinding?.model;
11081
+ if (!targetMongooseModel) {
11082
+ try {
11083
+ targetMongooseModel = this.requestRuntime.getModelInstance(meta.targetModelName);
11084
+ } catch {
11085
+ targetMongooseModel = parentContext.mongooseModel;
11086
+ }
11027
11087
  }
11028
11088
  const targetContext = {
11029
11089
  mongooseModel: targetMongooseModel ?? parentContext.mongooseModel,