@web-ts-toolkit/access-router 0.56.0 → 0.58.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.mjs CHANGED
@@ -2140,10 +2140,34 @@ var init_field_catalog = __esm({
2140
2140
  }
2141
2141
  });
2142
2142
 
2143
+ // src/acl/populate-target-configuration.ts
2144
+ import { isPlainObject as isPlainObject12 } from "@web-ts-toolkit/utils";
2145
+ function capturePopulateTargetConfiguration(runtime, modelName, source, proposedOptions) {
2146
+ const label = `Populate target "${modelName}" for ${source}`;
2147
+ if (!proposedOptions && !runtime.hasModel(modelName))
2148
+ authorizationError(
2149
+ `${label} must be configured first in this runtime; register its Mongoose model and setModelOptions with an object permissionSchema`
2150
+ );
2151
+ const model = runtime.getModelInstance(modelName);
2152
+ if (!model) authorizationError(`${label} requires an actual registered Mongoose model in this runtime`);
2153
+ const options = proposedOptions ?? runtime.getModelOptions(modelName);
2154
+ const permissionSchema = options.permissionSchema === void 0 ? runtime.getDefaultModelOption("permissionSchema") : options.permissionSchema;
2155
+ if (!isPlainObject12(permissionSchema))
2156
+ authorizationError(
2157
+ `${label} requires an effective object permissionSchema; configure the model or runtime model defaults ({} explicitly closes business fields)`
2158
+ );
2159
+ return { model, options: { ...options, permissionSchema } };
2160
+ }
2161
+ var init_populate_target_configuration = __esm({
2162
+ "src/acl/populate-target-configuration.ts"() {
2163
+ init_authorization_snapshot();
2164
+ }
2165
+ });
2166
+
2143
2167
  // src/acl/virtual-populate-dependencies.ts
2144
- import { get as get4, isPlainObject as isPlainObject12 } from "@web-ts-toolkit/utils";
2168
+ import { get as get4, isPlainObject as isPlainObject13 } from "@web-ts-toolkit/utils";
2145
2169
  function record(value, label) {
2146
- if (!isPlainObject12(value)) return fail(label, "entries must be plain { path, select? } objects");
2170
+ if (!isPlainObject13(value)) return fail(label, "entries must be plain { path, select? } objects");
2147
2171
  for (const key of Reflect.ownKeys(value)) {
2148
2172
  const property = Object.getOwnPropertyDescriptor(value, key);
2149
2173
  if (typeof key !== "string" || !property.enumerable || !("value" in property))
@@ -2232,7 +2256,8 @@ function normalizeSelection(select, catalog, label) {
2232
2256
  };
2233
2257
  }
2234
2258
  function captureVirtualPopulateDependencies(runtime, modelName, options, globalOptions = runtime.getGlobalOptions()) {
2235
- const requireReadOperationAccessForPopulateDependencies = globalOptions.requireReadOperationAccessForPopulateDependencies !== false;
2259
+ const requireOperationAccessForPopulateDependencies = globalOptions.requireOperationAccessForPopulateDependencies !== false;
2260
+ const requireOperationAccessForPopulateQueries = globalOptions.requireOperationAccessForPopulateQueries !== false;
2236
2261
  const model = runtime.getModelInstance(modelName);
2237
2262
  const targets = /* @__PURE__ */ new Map();
2238
2263
  const scopes = [];
@@ -2272,7 +2297,12 @@ function captureVirtualPopulateDependencies(runtime, modelName, options, globalO
2272
2297
  label,
2273
2298
  `target "${ref}" requires a registered same-connection non-discriminator ObjectId model bound to the source ref`
2274
2299
  );
2275
- const targetOptions = ref === modelName ? options : runtime.getModelOptions(ref);
2300
+ const { options: targetOptions } = capturePopulateTargetConfiguration(
2301
+ runtime,
2302
+ ref,
2303
+ `dependsOnPopulate virtual "${label}"`,
2304
+ ref === modelName ? options : void 0
2305
+ );
2276
2306
  const defaults = runtime.getDefaultModelOptions();
2277
2307
  const readOption = (key) => resolveNestedOption((path2, fallback) => get4(targetOptions, path2, fallback), key, get4(defaults, key));
2278
2308
  const documentPermissionField = targetOptions.documentPermissionField ?? defaults.documentPermissionField;
@@ -2281,7 +2311,9 @@ function captureVirtualPopulateDependencies(runtime, modelName, options, globalO
2281
2311
  {
2282
2312
  catalog,
2283
2313
  documentPermissionField,
2284
- requireReadOperationAccessForPopulateDependencies,
2314
+ requireOperationAccessForPopulateDependencies,
2315
+ requireOperationAccessForPopulateQueries,
2316
+ permissionSchema: targetOptions.permissionSchema,
2285
2317
  operationAccess: { read: readOption("operationAccess.read") },
2286
2318
  baseFilter: { read: readOption("baseFilter.read") },
2287
2319
  overrideFilter: { read: readOption("overrideFilter.read") },
@@ -2330,13 +2362,13 @@ function captureVirtualPopulateDependencies(runtime, modelName, options, globalO
2330
2362
  );
2331
2363
  continue;
2332
2364
  }
2333
- if (!isPlainObject12(raw)) continue;
2365
+ if (!isPlainObject13(raw)) continue;
2334
2366
  const entry = raw;
2335
2367
  const label = join(sourcePrefix, name);
2336
2368
  captureList(entry.dependsOnPopulate, label);
2337
2369
  if (!("get" in entry))
2338
2370
  for (const access of accesses) {
2339
- if (isPlainObject12(entry[access]))
2371
+ if (isPlainObject13(entry[access]))
2340
2372
  captureList(entry[access].dependsOnPopulate, `${label}.${access}`);
2341
2373
  }
2342
2374
  const byAccess = {};
@@ -2394,6 +2426,7 @@ var init_virtual_populate_dependencies = __esm({
2394
2426
  init_authorization_snapshot();
2395
2427
  init_field_catalog();
2396
2428
  init_virtual_descriptor();
2429
+ init_populate_target_configuration();
2397
2430
  elementType = (type) => type.$embeddedSchemaType ?? type.embeddedSchemaType ?? type.caster;
2398
2431
  join = (prefix, path) => prefix ? `${prefix}.${path}` : path;
2399
2432
  within = (path, parent) => path === parent || path.startsWith(`${parent}.`);
@@ -2418,7 +2451,7 @@ import {
2418
2451
  isBoolean,
2419
2452
  isFunction as isFunction2,
2420
2453
  isNil as isNil2,
2421
- isPlainObject as isPlainObject13,
2454
+ isPlainObject as isPlainObject14,
2422
2455
  isString as isString4,
2423
2456
  keys,
2424
2457
  normalizeUrlPath,
@@ -2485,7 +2518,7 @@ var init_runtime = __esm({
2485
2518
  const modelPermissionKeys = {};
2486
2519
  forEach2(schemaKeys, (schemaKey) => {
2487
2520
  const schemaRule = permissionSchema[schemaKey];
2488
- const ruleEntries = isPlainObject13(schemaRule) ? Object.entries(schemaRule) : FIELD_ACCESS_KEYS.map((accessKey) => [accessKey, schemaRule]);
2521
+ const ruleEntries = isPlainObject14(schemaRule) ? Object.entries(schemaRule) : FIELD_ACCESS_KEYS.map((accessKey) => [accessKey, schemaRule]);
2489
2522
  for (let x = 0; x < ruleEntries.length; x++) {
2490
2523
  const [accessKey, value] = ruleEntries[x];
2491
2524
  if (!isArray5(globalPermissionKeys[accessKey])) {
@@ -2526,7 +2559,7 @@ var init_runtime = __esm({
2526
2559
  VIRTUAL_RESERVED_NAMES = /* @__PURE__ */ new Set(["_id", "__v", "id"]);
2527
2560
  VIRTUAL_DANGEROUS_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
2528
2561
  VIRTUAL_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
2529
- isVirtualContainerValue2 = (value) => isPlainObject13(value) && "sub" in value;
2562
+ isVirtualContainerValue2 = (value) => isPlainObject14(value) && "sub" in value;
2530
2563
  virtualFullPath = (prefix, name) => prefix ? `${prefix}.${name}` : name;
2531
2564
  virtualPathsOverlap = (virtualPath, permissionField) => {
2532
2565
  if (!virtualPath || !permissionField) return false;
@@ -2621,7 +2654,7 @@ var init_runtime = __esm({
2621
2654
  };
2622
2655
  validateVirtualLeafValue = (value, entryLabel, storedFields, siblingVirtualNames, sharedDependsOn) => {
2623
2656
  if (isFunction2(value)) return;
2624
- if (!isPlainObject13(value)) {
2657
+ if (!isPlainObject14(value)) {
2625
2658
  throw new Error(`Virtual ${entryLabel}: descriptor must be a getter function or { get, dependsOn }`);
2626
2659
  }
2627
2660
  const record3 = value;
@@ -2667,7 +2700,7 @@ var init_runtime = __esm({
2667
2700
  `Virtual ${scopeLabel}${name}: scope-container must only hold "sub" (embedded container "${name}" is not a virtual-leaf collision)`
2668
2701
  );
2669
2702
  }
2670
- if (!isPlainObject13(record4.sub)) {
2703
+ if (!isPlainObject14(record4.sub)) {
2671
2704
  throw new Error(`Virtual ${scopeLabel}${name}: scope-container "sub" must be a plain object`);
2672
2705
  }
2673
2706
  continue;
@@ -2678,7 +2711,7 @@ var init_runtime = __esm({
2678
2711
  );
2679
2712
  }
2680
2713
  if (isFunction2(raw)) continue;
2681
- if (!isPlainObject13(raw)) {
2714
+ if (!isPlainObject14(raw)) {
2682
2715
  throw new Error(
2683
2716
  `Virtual ${scopeLabel}${name}: malformed descriptor (expected getter, { get, dependsOn }, or per-access record)`
2684
2717
  );
@@ -2732,7 +2765,7 @@ var init_runtime = __esm({
2732
2765
  };
2733
2766
  validateVirtualsTree = (model, virtuals, documentPermissionField, modelName) => {
2734
2767
  if (virtuals === void 0 || virtuals === null) return;
2735
- if (!isPlainObject13(virtuals)) {
2768
+ if (!isPlainObject14(virtuals)) {
2736
2769
  throw new Error(`Virtuals for model "${modelName}": virtuals must be a plain object mapping names to descriptors`);
2737
2770
  }
2738
2771
  const rootSchema = model?.schema ?? null;
@@ -2784,8 +2817,8 @@ var init_runtime = __esm({
2784
2817
  {
2785
2818
  requestPermissionField: "_permissions",
2786
2819
  globalPermissions: () => ({}),
2787
- requireRegisteredPopulateModels: true,
2788
- requireReadOperationAccessForPopulateDependencies: true,
2820
+ requireOperationAccessForPopulateQueries: true,
2821
+ requireOperationAccessForPopulateDependencies: true,
2789
2822
  logger: defaultLogger,
2790
2823
  requestComplexity: defaultRequestComplexity
2791
2824
  },
@@ -2959,13 +2992,15 @@ var init_runtime = __esm({
2959
2992
  /** Shallowly replace supplied global values; nested objects such as requestComplexity are not deep-merged. */
2960
2993
  setGlobalOptions(options) {
2961
2994
  const next = { ...options };
2962
- if ("requireReadOperationAccessForPopulateDependencies" in next)
2963
- next.requireReadOperationAccessForPopulateDependencies = next.requireReadOperationAccessForPopulateDependencies !== false;
2995
+ if ("requireOperationAccessForPopulateQueries" in next)
2996
+ next.requireOperationAccessForPopulateQueries = next.requireOperationAccessForPopulateQueries !== false;
2997
+ if ("requireOperationAccessForPopulateDependencies" in next)
2998
+ next.requireOperationAccessForPopulateDependencies = next.requireOperationAccessForPopulateDependencies !== false;
2964
2999
  this.globalOptions.assign(next);
2965
3000
  }
2966
3001
  setGlobalOption(key, value) {
2967
- if (key === "requireReadOperationAccessForPopulateDependencies") {
2968
- this.globalOptions.set("requireReadOperationAccessForPopulateDependencies", value !== false);
3002
+ if (key === "requireOperationAccessForPopulateQueries" || key === "requireOperationAccessForPopulateDependencies") {
3003
+ this.globalOptions.set(key, value !== false);
2969
3004
  return;
2970
3005
  }
2971
3006
  this.globalOptions.set(key, value);
@@ -3088,10 +3123,10 @@ var init_runtime = __esm({
3088
3123
  const segments = nestedPath.split(".");
3089
3124
  let cursor = cloned;
3090
3125
  for (let i = 0; i < segments.length - 1; i++) {
3091
- if (!isPlainObject13(cursor)) break;
3126
+ if (!isPlainObject14(cursor)) break;
3092
3127
  cursor = cursor[segments[i]];
3093
3128
  }
3094
- if (isPlainObject13(cursor)) {
3129
+ if (isPlainObject14(cursor)) {
3095
3130
  delete cursor[segments[segments.length - 1]];
3096
3131
  }
3097
3132
  proposedVirtuals = cloned;
@@ -3499,6 +3534,47 @@ function serviceAuthorizationInput(policy, targets) {
3499
3534
  }
3500
3535
  return { rows: policy.rows, outputFields: policy.outputFields, related };
3501
3536
  }
3537
+ function hasStableConfigurationCopies(value) {
3538
+ if (value === null || typeof value !== "object") return true;
3539
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype) return false;
3540
+ return Object.values(value).every(hasStableConfigurationCopies);
3541
+ }
3542
+ function captureAuthorizationAccess(value) {
3543
+ const input = authorizationRecord(value, "access");
3544
+ const access = {};
3545
+ for (const key of ["operation", "rowAccess", "virtualAccess", "outputAccess", "docPermissionsAccess"]) {
3546
+ if (key === "rowAccess" && input[key] === void 0) continue;
3547
+ if (typeof input[key] !== "string") authorizationError(`invalid access.${key}`);
3548
+ access[key] = input[key];
3549
+ }
3550
+ return Object.freeze(access);
3551
+ }
3552
+ function captureAuthorizationCatalog(value) {
3553
+ const input = authorizationRecord(value, "catalog");
3554
+ const catalog = {};
3555
+ for (const key of [
3556
+ "paths",
3557
+ "persistedPaths",
3558
+ "arrayPaths",
3559
+ "schemaVirtualPaths",
3560
+ "metadataPaths",
3561
+ "dynamicPaths",
3562
+ "containerPaths",
3563
+ "virtualPaths",
3564
+ "virtualScopePaths",
3565
+ "opaquePaths",
3566
+ "referencePaths",
3567
+ "protectedPaths"
3568
+ ]) {
3569
+ const paths = input[key];
3570
+ if (key !== "paths" && paths === void 0) continue;
3571
+ if (!Array.isArray(paths) || Array.from(paths).some((path) => typeof path !== "string")) {
3572
+ authorizationError(`invalid catalog.${key}`);
3573
+ }
3574
+ catalog[key] = paths;
3575
+ }
3576
+ return Object.freeze(catalog);
3577
+ }
3502
3578
  function assertAuthorizationPath(path) {
3503
3579
  if (typeof path !== "string" || !isSafeOutputPath(path)) {
3504
3580
  authorizationError(`invalid response path: ${String(path)}`);
@@ -3583,7 +3659,6 @@ function normalizePolicy(value, applicability, configuration, targets) {
3583
3659
  const matches = targets.filter((target2) => target2.path === path);
3584
3660
  if (matches.length !== 1) authorizationError(`unused or ambiguous related path: ${path}`);
3585
3661
  const target = matches[0];
3586
- if (target.kind === "passthrough") authorizationError(`related policy requires a registered target: ${path}`);
3587
3662
  const child = authorizationRecord(entry, `related.${path}`);
3588
3663
  assertAuthorizationKeys(
3589
3664
  child,
@@ -3629,6 +3704,9 @@ function resolveServiceAuthorization(input) {
3629
3704
  const raw = copyAuthorizationValue(input.authorization);
3630
3705
  const configuration = copyAuthorizationValue(input.configuration, true);
3631
3706
  const targets = copyAuthorizationValue(input.relatedTargets ?? [], true);
3707
+ const access = copyAuthorizationValue(input.access);
3708
+ const outputAccess = captureAuthorizationAccess(access);
3709
+ const catalog = captureAuthorizationCatalog(configuration.catalog);
3632
3710
  const policy = copyAuthorizationValue(normalizePolicy(raw, input.applicability ?? "service", configuration, targets));
3633
3711
  const scopePath = Array.from(scope(input.binding));
3634
3712
  for (const part of scopePath) {
@@ -3641,17 +3719,21 @@ function resolveServiceAuthorization(input) {
3641
3719
  authorizationError("derived call request/runtime mismatch");
3642
3720
  }
3643
3721
  }
3644
- const permissions = Object.fromEntries(
3645
- input.requestPermissions.keys.map((key) => [key, input.requestPermissions.has(key)])
3722
+ const permissions = new permission_default(
3723
+ Object.fromEntries(input.requestPermissions.keys.map((key) => [key, input.requestPermissions.has(key) === true]))
3646
3724
  );
3725
+ for (const value of Object.values(permissions)) if (value && typeof value === "object") Object.freeze(value);
3726
+ Object.freeze(permissions);
3647
3727
  const call = Object.freeze(/* @__PURE__ */ Object.create(null));
3648
3728
  calls.set(call, {
3649
3729
  binding: Object.freeze({ ...input.binding, scopePath: Object.freeze(scopePath) }),
3650
- access: copyAuthorizationValue(input.access),
3730
+ access,
3651
3731
  configuration,
3652
3732
  policy,
3653
3733
  relatedTargets: targets,
3654
- permissions: Object.freeze(permissions),
3734
+ // normalizeFields constructs only mode strings and validated string paths.
3735
+ output: Object.freeze({ access: outputAccess, catalog, outputFields: policy.outputFields }),
3736
+ permissions,
3655
3737
  provenance: input.parent ? callRecord(input.parent).provenance : call
3656
3738
  });
3657
3739
  return call;
@@ -3669,12 +3751,75 @@ function readServiceAuthorization(call, binding) {
3669
3751
  true
3670
3752
  );
3671
3753
  }
3754
+ function readServiceAuthorizationAccess(call, binding) {
3755
+ assertAuthorizationBinding(call, binding);
3756
+ return callRecord(call).output.access;
3757
+ }
3758
+ function readServiceAuthorizationHydration(call, binding) {
3759
+ assertAuthorizationBinding(call, binding);
3760
+ return copyAuthorizationValue({ hydration: callRecord(call).configuration.hydration }, true);
3761
+ }
3762
+ function outputMetadata(configuration) {
3763
+ return {
3764
+ documentPermissionField: configuration.documentPermissionField,
3765
+ exposedDocPermissionKeys: configuration.exposedDocPermissionKeys,
3766
+ stripPermissionsField: configuration.stripPermissionsField,
3767
+ disableFieldPermissions: configuration.disableFieldPermissions
3768
+ };
3769
+ }
3770
+ function readServiceOutputMetadata(call, binding) {
3771
+ assertAuthorizationBinding(call, binding);
3772
+ return copyAuthorizationValue(outputMetadata(callRecord(call).configuration), true);
3773
+ }
3774
+ function readServiceDocumentPermissions(call, binding) {
3775
+ assertAuthorizationBinding(call, binding);
3776
+ const record3 = callRecord(call);
3777
+ return Object.freeze({
3778
+ catalog: record3.output.catalog,
3779
+ ...copyAuthorizationValue({ docPermissions: record3.configuration.docPermissions }, true)
3780
+ });
3781
+ }
3782
+ function readServiceFinalizerConfiguration(call, binding) {
3783
+ assertAuthorizationBinding(call, binding);
3784
+ const record3 = callRecord(call);
3785
+ return Object.freeze({
3786
+ catalog: record3.output.catalog,
3787
+ ...copyAuthorizationValue(
3788
+ {
3789
+ ...outputMetadata(record3.configuration),
3790
+ virtuals: record3.configuration.virtuals,
3791
+ docPermissions: record3.configuration.docPermissions
3792
+ },
3793
+ true
3794
+ )
3795
+ });
3796
+ }
3797
+ function readServiceOutputPolicy(call, binding) {
3798
+ assertAuthorizationBinding(call, binding);
3799
+ return callRecord(call).output;
3800
+ }
3801
+ function readServiceOutputFieldAuthorization(call, binding) {
3802
+ assertAuthorizationBinding(call, binding);
3803
+ const record3 = callRecord(call);
3804
+ const reusable = record3.reusableFieldConfiguration ??= hasStableConfigurationCopies(record3.configuration.permissionSchema) && // Coercion methods can observe an object's identity even without native
3805
+ // leaves. Keep those receivers local to the original resolution boundary.
3806
+ (record3.configuration.modelPermissionPrefix === null || typeof record3.configuration.modelPermissionPrefix !== "object");
3807
+ return Object.freeze({
3808
+ access: record3.output.access,
3809
+ outputFields: record3.output.outputFields,
3810
+ reusable,
3811
+ ...copyAuthorizationValue(
3812
+ {
3813
+ permissionSchema: record3.configuration.permissionSchema,
3814
+ modelPermissionPrefix: record3.configuration.modelPermissionPrefix
3815
+ },
3816
+ true
3817
+ )
3818
+ });
3819
+ }
3672
3820
  function getAuthorizationPermissions(call, binding) {
3673
3821
  assertAuthorizationBinding(call, binding);
3674
- const permission = new permission_default({ ...callRecord(call).permissions });
3675
- for (const value of Object.values(permission)) if (value && typeof value === "object") Object.freeze(value);
3676
- Object.freeze(permission);
3677
- return permission;
3822
+ return callRecord(call).permissions;
3678
3823
  }
3679
3824
  function getAuthorizationProvenance(call) {
3680
3825
  return callRecord(call).provenance;
@@ -3714,7 +3859,7 @@ function normalizeOutputAuthorizationOptions(input, options, purpose) {
3714
3859
  const stage = stageRecord(documentRecord(handle).stage);
3715
3860
  const binding = Object.freeze({ ...input.binding, scopePath: stage.binding.scopePath });
3716
3861
  assertAuthorizationBinding(stage.call, binding);
3717
- const access = readServiceAuthorization(stage.call, binding).access;
3862
+ const access = readServiceAuthorizationAccess(stage.call, binding);
3718
3863
  if (input.access.outputAccess !== access.outputAccess) authorizationError("document access mismatch");
3719
3864
  return Object.freeze({ kind: "document", documentAuthorization: handle, binding });
3720
3865
  }
@@ -3792,7 +3937,16 @@ function readDocumentAuthorization(handle, binding) {
3792
3937
  authorization: stage.call,
3793
3938
  stage: record3.stage,
3794
3939
  grants: copyAuthorizationValue(record3.grants),
3795
- ...readServiceAuthorization(stage.call, binding).access
3940
+ ...readServiceAuthorizationAccess(stage.call, binding)
3941
+ });
3942
+ }
3943
+ function readDocumentAuthorizationAccess(handle, binding) {
3944
+ const record3 = documentRecord(handle);
3945
+ const stage = stageRecord(record3.stage);
3946
+ return Object.freeze({
3947
+ authorization: stage.call,
3948
+ stage: record3.stage,
3949
+ ...readServiceAuthorizationAccess(stage.call, binding)
3796
3950
  });
3797
3951
  }
3798
3952
  function memoizeDocumentFieldDecision(handle, binding, field, evaluate) {
@@ -3800,15 +3954,23 @@ function memoizeDocumentFieldDecision(handle, binding, field, evaluate) {
3800
3954
  const stage = stageRecord(record3.stage);
3801
3955
  assertAuthorizationBinding(stage.call, binding);
3802
3956
  let decision = record3.decisions.get(field);
3803
- if (!decision) {
3957
+ if (decision) {
3958
+ return decision.then((result2) => {
3959
+ stageRecord(record3.stage);
3960
+ assertAuthorizationBinding(stage.call, binding);
3961
+ return result2;
3962
+ });
3963
+ } else {
3804
3964
  decision = Promise.resolve().then(async () => {
3805
3965
  stageRecord(record3.stage);
3966
+ assertAuthorizationBinding(stage.call, binding);
3806
3967
  const result2 = await evaluate.call(
3807
3968
  stage.binding.request,
3808
3969
  getAuthorizationPermissions(stage.call, binding),
3809
3970
  copyAuthorizationValue(record3.grants)
3810
3971
  );
3811
3972
  stageRecord(record3.stage);
3973
+ assertAuthorizationBinding(stage.call, binding);
3812
3974
  if (typeof result2 !== "boolean") authorizationError("field decision must be boolean");
3813
3975
  return result2;
3814
3976
  });
@@ -3825,7 +3987,7 @@ function assertDocumentAuthorization(handle, use) {
3825
3987
  const record3 = documentRecord(handle);
3826
3988
  const stage = stageRecord(record3.stage);
3827
3989
  assertAuthorizationBinding(stage.call, use.binding);
3828
- if (use.authorization !== void 0 && stage.call !== use.authorization || use.stage !== void 0 && record3.stage !== use.stage || readServiceAuthorization(stage.call, use.binding).access.outputAccess !== use.outputAccess) {
3990
+ if (use.authorization !== void 0 && stage.call !== use.authorization || use.stage !== void 0 && record3.stage !== use.stage || readServiceAuthorizationAccess(stage.call, use.binding).outputAccess !== use.outputAccess) {
3829
3991
  authorizationError("document call/access/stage mismatch");
3830
3992
  }
3831
3993
  const association = outputs.get(use.output);
@@ -3880,7 +4042,7 @@ function transferDocumentAuthorizationGraph(source, copy, copyOrigins) {
3880
4042
  {
3881
4043
  binding: stage.binding,
3882
4044
  output: original,
3883
- outputAccess: readServiceAuthorization(stage.call, stage.binding).access.outputAccess,
4045
+ outputAccess: readServiceAuthorizationAccess(stage.call, stage.binding).outputAccess,
3884
4046
  authorization: stage.call,
3885
4047
  stage: documentRecord(association.handle).stage
3886
4048
  },
@@ -4044,12 +4206,20 @@ function classifyRequestFieldRule(rule, permissions, modelPermissionPrefix = "")
4044
4206
  );
4045
4207
  return (typeof rule === "string" ? possible.stringHandler(rule) : possible.arrayHandler(rule)) ? "deferred" : "denied";
4046
4208
  }
4209
+ function prepareRequestFieldRule(rule, permissions, modelPermissionPrefix = "") {
4210
+ const state = classifyRequestFieldRule(rule, permissions, modelPermissionPrefix);
4211
+ const invariant = state === "denied" && modelPermissionPrefix ? classifyRequestFieldRule(rule, permissions) : state;
4212
+ return Object.freeze({ state, decision: invariant === "deferred" ? void 0 : invariant === "granted" });
4213
+ }
4214
+ function combineFieldAuthorizationStates(states2) {
4215
+ return states2.includes("denied") ? "denied" : states2.includes("deferred") ? "deferred" : "granted";
4216
+ }
4047
4217
  function classifyEffectiveFieldAuthorization(args) {
4048
4218
  if (hasExplicitFieldGrant(args.outputFields, args.path)) return "granted";
4049
4219
  const states2 = args.rules.map(
4050
4220
  (rule) => classifyRequestFieldRule(rule, args.globalPermissions, args.modelPermissionPrefix)
4051
4221
  );
4052
- return states2.includes("denied") ? "denied" : states2.includes("deferred") ? "deferred" : "granted";
4222
+ return combineFieldAuthorizationStates(states2);
4053
4223
  }
4054
4224
  async function evaluateScopedAccessRule(args) {
4055
4225
  const { req, rule, globalPermissions, docPermissions, modelPermissionPrefix } = args;
@@ -4093,7 +4263,7 @@ var init_field_authorization = __esm({
4093
4263
  });
4094
4264
 
4095
4265
  // src/acl/virtual-projection.ts
4096
- import { get as get6, isArray as isArray7, isPlainObject as isPlainObject15 } from "@web-ts-toolkit/utils";
4266
+ import { get as get6, isArray as isArray7, isPlainObject as isPlainObject16 } from "@web-ts-toolkit/utils";
4097
4267
  function planVirtualProjection(args) {
4098
4268
  if (args.snapshot.catalog) return planCatalogProjection(args);
4099
4269
  const {
@@ -4327,15 +4497,15 @@ var init_virtual_projection = __esm({
4327
4497
  init_virtual_populate_dependencies();
4328
4498
  init_virtual_descriptor();
4329
4499
  getScopedPermissionSchema = (root, scopePath) => {
4330
- if (!root || !isPlainObject15(root)) return null;
4500
+ if (!root || !isPlainObject16(root)) return null;
4331
4501
  if (scopePath.length === 0) return root;
4332
4502
  const scoped = get6(root, scopePath.join("."));
4333
- return isPlainObject15(scoped) ? scoped : null;
4503
+ return isPlainObject16(scoped) ? scoped : null;
4334
4504
  };
4335
4505
  resolveAlwaysSelectForAccess = (raw, access) => {
4336
4506
  if (!raw) return [];
4337
4507
  if (isArray7(raw)) return [...raw];
4338
- if (isPlainObject15(raw)) {
4508
+ if (isPlainObject16(raw)) {
4339
4509
  const record3 = raw;
4340
4510
  const exact = record3[access];
4341
4511
  if (isArray7(exact)) return [...exact];
@@ -4622,9 +4792,9 @@ function captureModelAuthorizationConfiguration(runtime, modelName, options = ru
4622
4792
  options,
4623
4793
  globalOptions
4624
4794
  ),
4625
- requireReadOperationAccessForPopulateDependencies: globalOptions.requireReadOperationAccessForPopulateDependencies !== false,
4626
- requireRegisteredPopulateModels: globalOptions.requireRegisteredPopulateModels,
4627
- permissionSchema: options.permissionSchema,
4795
+ requireOperationAccessForPopulateDependencies: globalOptions.requireOperationAccessForPopulateDependencies !== false,
4796
+ requireOperationAccessForPopulateQueries: globalOptions.requireOperationAccessForPopulateQueries !== false,
4797
+ permissionSchema: options.permissionSchema === void 0 ? runtime.getDefaultModelOption("permissionSchema") : options.permissionSchema,
4628
4798
  virtuals,
4629
4799
  alwaysSelectFields: options.alwaysSelectFields,
4630
4800
  modelPermissionPrefix: options.modelPermissionPrefix,
@@ -4690,15 +4860,15 @@ import {
4690
4860
  isBoolean as isBoolean2,
4691
4861
  isEmpty as isEmpty2,
4692
4862
  isFunction as isFunction3,
4693
- isPlainObject as isPlainObject16,
4863
+ isPlainObject as isPlainObject17,
4694
4864
  isString as isString5
4695
4865
  } from "@web-ts-toolkit/utils";
4696
4866
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
4697
4867
  function isAndFilter(filter2) {
4698
- return !!filter2 && isPlainObject16(filter2) && Object.keys(filter2).length === 1 && isArray8(filter2.$and);
4868
+ return !!filter2 && isPlainObject17(filter2) && Object.keys(filter2).length === 1 && isArray8(filter2.$and);
4699
4869
  }
4700
4870
  function isMergeableClause(filter2) {
4701
- return !!filter2 && isPlainObject16(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
4871
+ return !!filter2 && isPlainObject17(filter2) && Object.keys(filter2).every((key) => !key.startsWith("$"));
4702
4872
  }
4703
4873
  function optimizeAndFilter(clausesInput) {
4704
4874
  const clauses = [];
@@ -4719,7 +4889,7 @@ function optimizeAndFilter(clausesInput) {
4719
4889
  const entries = Array.from(value, dedupeKey);
4720
4890
  return entries.every((entry) => entry !== void 0) ? `array:${JSON.stringify(entries)}` : void 0;
4721
4891
  }
4722
- if (isPlainObject16(value)) {
4892
+ if (isPlainObject17(value)) {
4723
4893
  const entries = Object.keys(value).sort().map((key) => [key, dedupeKey(value[key])]);
4724
4894
  return entries.every(([, entry]) => entry !== void 0) ? `object:${JSON.stringify(entries)}` : void 0;
4725
4895
  }
@@ -4767,7 +4937,7 @@ function optimizeAndFilter(clausesInput) {
4767
4937
  }
4768
4938
  function normalizeFilter(filter2) {
4769
4939
  if (filter2 === false) return false;
4770
- if (!filter2 || !isPlainObject16(filter2) || isEmpty2(filter2)) return null;
4940
+ if (!filter2 || !isPlainObject17(filter2) || isEmpty2(filter2)) return null;
4771
4941
  if (!isAndFilter(filter2)) {
4772
4942
  return filter2;
4773
4943
  }
@@ -4845,7 +5015,7 @@ async function setRequestPermissions(req) {
4845
5015
  if (hasFieldValue && owner === void 0) return;
4846
5016
  const globalPermissions = getGlobalOption("globalPermissions");
4847
5017
  const permissions = isFunction3(globalPermissions) ? await globalPermissions.call(req, req) : void 0;
4848
- if (isPlainObject16(permissions)) req[requestPermissionField] = permissions;
5018
+ if (isPlainObject17(permissions)) req[requestPermissionField] = permissions;
4849
5019
  else if (isArray8(permissions)) req[requestPermissionField] = arrayToRecord(permissions);
4850
5020
  else if (isString5(permissions)) req[requestPermissionField] = { [permissions]: true };
4851
5021
  else req[requestPermissionField] = {};
@@ -5244,7 +5414,7 @@ var init_filter_resolution = __esm({
5244
5414
  // src/acl/virtual-populate-execution.ts
5245
5415
  import { Document as Document4, mongo as mongo4 } from "mongoose";
5246
5416
  import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
5247
- import { isPlainObject as isPlainObject19 } from "@web-ts-toolkit/utils";
5417
+ import { isPlainObject as isPlainObject20 } from "@web-ts-toolkit/utils";
5248
5418
  function projectVirtualPopulateView(source, selection, structurePaths = []) {
5249
5419
  const root = { children: /* @__PURE__ */ new Map() };
5250
5420
  for (const path of compactPersistedProjection(selection)) {
@@ -5276,8 +5446,8 @@ function equivalentVirtualPopulatePredicate(left, right) {
5276
5446
  }
5277
5447
  if (Array.isArray(left) || Array.isArray(right))
5278
5448
  return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((entry, index) => equivalentVirtualPopulatePredicate(entry, right[index]));
5279
- if (isPlainObject19(left) || isPlainObject19(right)) {
5280
- if (!isPlainObject19(left) || !isPlainObject19(right)) return false;
5449
+ if (isPlainObject20(left) || isPlainObject20(right)) {
5450
+ if (!isPlainObject20(left) || !isPlainObject20(right)) return false;
5281
5451
  const a = Object.keys(left), b = Object.keys(right);
5282
5452
  return a.length === b.length && a.every(
5283
5453
  (key, index) => key === b[index] && equivalentVirtualPopulatePredicate(
@@ -5348,7 +5518,6 @@ function captureVirtualPopulateRows(batch, inputs, plan, publicPopulate = [], so
5348
5518
  address,
5349
5519
  plan: currentPlan,
5350
5520
  refs: /* @__PURE__ */ new Map(),
5351
- referenceTargets: /* @__PURE__ */ new Map(),
5352
5521
  dependencies: /* @__PURE__ */ new Map()
5353
5522
  };
5354
5523
  row.occurrences.set(addressKey(address), occurrence);
@@ -5356,7 +5525,6 @@ function captureVirtualPopulateRows(batch, inputs, plan, publicPopulate = [], so
5356
5525
  const dependencies = (candidate.populateDependencies ?? []).map((dependency) => {
5357
5526
  if (!occurrence.refs.has(dependency.path))
5358
5527
  occurrence.refs.set(dependency.path, originalRef(value, dependency.path));
5359
- occurrence.referenceTargets.set(dependency.path, dependency.target);
5360
5528
  const request = {
5361
5529
  binding: dependency.target,
5362
5530
  target: dependency.target,
@@ -5454,7 +5622,7 @@ async function execute(state) {
5454
5622
  if (!pending) {
5455
5623
  pending = (async () => {
5456
5624
  try {
5457
- if (target.configuration.requireReadOperationAccessForPopulateDependencies !== false && !await targetReadAllowed(state, target))
5625
+ if (target.configuration.requireOperationAccessForPopulateDependencies !== false && !await targetReadAllowed(state, target))
5458
5626
  return void 0;
5459
5627
  const parts = await work.hookGate.run(
5460
5628
  () => resolveAccessFilterParts({
@@ -5540,12 +5708,6 @@ function readVirtualPopulateOriginalReference(row, address, path) {
5540
5708
  function hasVirtualPopulatePublicReference(row, address, path) {
5541
5709
  return !!rows.get(row)?.publicRequests.some((request) => addressKey(request.address) === addressKey([...address, path]));
5542
5710
  }
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
- }
5549
5711
  function readVirtualPopulateInputs(row, address, virtualName) {
5550
5712
  const record3 = rows.get(row);
5551
5713
  if (!record3?.batch.completed) return { available: false };
@@ -5688,7 +5850,7 @@ var init_virtual_populate_execution = __esm({
5688
5850
  publicViewIdentities = /* @__PURE__ */ new WeakMap();
5689
5851
  familyRawFetches = /* @__PURE__ */ new WeakMap();
5690
5852
  addressKey = (address) => JSON.stringify(address);
5691
- storage = (value) => value instanceof Document4 ? value._doc : isPlainObject19(value) ? value : void 0;
5853
+ storage = (value) => value instanceof Document4 ? value._doc : isPlainObject20(value) ? value : void 0;
5692
5854
  originalRef = (source, key) => {
5693
5855
  const record3 = storage(source);
5694
5856
  return {
@@ -5701,7 +5863,7 @@ var init_virtual_populate_execution = __esm({
5701
5863
  const option = getCapturedAuthorizationOption(configuration, `${key}.${access}`);
5702
5864
  return key === "operationAccess" || typeof option === "function" ? option : void 0;
5703
5865
  });
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.
5866
+ 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.requireOperationAccessForPopulateDependencies === b.binding.configuration.requireOperationAccessForPopulateDependencies && // Different captures may share only an equivalent terminal persisted binding/policy.
5705
5867
  equivalentVirtualPopulatePredicate(
5706
5868
  a.binding.configuration.catalog.persistedPaths,
5707
5869
  b.binding.configuration.catalog.persistedPaths
@@ -5715,21 +5877,116 @@ var init_virtual_populate_execution = __esm({
5715
5877
  });
5716
5878
 
5717
5879
  // src/acl/output-field-filter.ts
5718
- import { isPlainObject as isPlainObject20 } from "@web-ts-toolkit/utils";
5880
+ import { isPlainObject as isPlainObject21 } from "@web-ts-toolkit/utils";
5881
+ function callFieldPolicy(authorization, binding) {
5882
+ assertAuthorizationBinding(authorization, binding);
5883
+ let policy = fieldPolicies.get(authorization);
5884
+ if (!policy) {
5885
+ const view = readServiceOutputFieldAuthorization(authorization, binding);
5886
+ policy = {
5887
+ schema: view.permissionSchema,
5888
+ requestPaths: view.permissionSchema == null || isPlainObject21(view.permissionSchema) && Object.isFrozen(view.permissionSchema) ? Object.freeze(Object.keys(view.permissionSchema ?? {})) : void 0,
5889
+ prefix: view.modelPermissionPrefix ?? "",
5890
+ outputFields: view.outputFields,
5891
+ accesses: /* @__PURE__ */ new Map()
5892
+ };
5893
+ if (view.reusable) fieldPolicies.set(authorization, policy);
5894
+ }
5895
+ return policy;
5896
+ }
5897
+ function requestRuleClassification(entry, authorization, permissions, prefix) {
5898
+ if (typeof prefix !== "string") return void 0;
5899
+ let classification = entry.classifications.get(authorization);
5900
+ if (!classification) {
5901
+ classification = prepareRequestFieldRule(entry.rule, permissions, prefix);
5902
+ entry.classifications.set(authorization, classification);
5903
+ }
5904
+ return classification;
5905
+ }
5906
+ function invariantRuleLookup(schema, path) {
5907
+ if (schema == null) return true;
5908
+ if (!isPlainObject21(schema) || !Object.isFrozen(schema)) return false;
5909
+ const exact = Object.getOwnPropertyDescriptor(schema, path);
5910
+ if (exact) {
5911
+ const raw2 = exact.value;
5912
+ return !raw2 || typeof raw2 !== "object" || Array.isArray(raw2) || isPlainObject21(raw2) && Object.isFrozen(raw2);
5913
+ }
5914
+ const [head, ...rest] = path.split(".");
5915
+ const raw = Object.getOwnPropertyDescriptor(schema, head)?.value;
5916
+ if (raw == null) return true;
5917
+ if (!isPlainObject21(raw) || !Object.isFrozen(raw)) return false;
5918
+ const sub = Object.getOwnPropertyDescriptor(raw, "sub");
5919
+ return !rest.length || !sub || invariantRuleLookup(sub.value, rest.join("."));
5920
+ }
5921
+ function prepareOutputPath(policy, authorization, binding, path, access) {
5922
+ let cache = policy.accesses.get(access);
5923
+ if (!cache) {
5924
+ cache = /* @__PURE__ */ new Map();
5925
+ policy.accesses.set(access, cache);
5926
+ }
5927
+ let prepared = cache.get(path);
5928
+ if (!prepared) {
5929
+ const invariant = invariantRuleLookup(policy.schema, path);
5930
+ const permissions = getAuthorizationPermissions(authorization, binding);
5931
+ const entries = extractOutputFieldRuleEntries(policy.schema, path, access).map((entry) => ({
5932
+ ...entry,
5933
+ classifications: /* @__PURE__ */ new WeakMap()
5934
+ }));
5935
+ const classifications = entries.map(
5936
+ (entry) => requestRuleClassification(entry, authorization, permissions, policy.prefix)
5937
+ );
5938
+ const backendGrant = hasExplicitFieldGrant(policy.outputFields, path);
5939
+ prepared = {
5940
+ entries,
5941
+ backendGrant,
5942
+ state: backendGrant ? "granted" : classifications.every((value) => value !== void 0) ? combineFieldAuthorizationStates(classifications.map((value) => value.state)) : void 0
5943
+ };
5944
+ if (invariant) cache.set(path, prepared);
5945
+ }
5946
+ return prepared;
5947
+ }
5948
+ function assertOwningOutputDocuments(handle) {
5949
+ const owner = owningDocuments.get(handle);
5950
+ if (owner) {
5951
+ readDocumentAuthorizationAccess(owner.handle, owner.binding);
5952
+ assertOwningOutputDocuments(owner.handle);
5953
+ }
5954
+ }
5955
+ function prepareOutputDocumentAuthorization(authorization, binding, configuration) {
5956
+ const access = readServiceAuthorizationAccess(authorization, binding);
5957
+ const key = `docPermissions.${access.docPermissionsAccess}`;
5958
+ const docPermissions = configuration.docPermissions;
5959
+ const invariant = /^[A-Za-z0-9_-]+$/.test(access.docPermissionsAccess) && (docPermissions == null || isPlainObject21(docPermissions));
5960
+ const option = invariant ? getCapturedAuthorizationOption(configuration, key) : void 0;
5961
+ const hook = typeof option === "function" ? option : void 0;
5962
+ const resolveHook = invariant ? () => hook : () => {
5963
+ const slice = { catalog: configuration.catalog, ...copyAuthorizationValue({ docPermissions }, true) };
5964
+ const option2 = getCapturedAuthorizationOption(slice, key);
5965
+ return typeof option2 === "function" ? option2 : void 0;
5966
+ };
5967
+ return async (args) => {
5968
+ if (args.authorization !== authorization) authorizationError("document resolver call mismatch");
5969
+ assertAuthorizationBinding(authorization, args.binding);
5970
+ return resolveOutputDocumentAuthorizationWithHook(args, access, resolveHook);
5971
+ };
5972
+ }
5719
5973
  async function resolveOutputDocumentAuthorization(args) {
5974
+ const configuration = readServiceDocumentPermissions(args.authorization, args.binding);
5975
+ return prepareOutputDocumentAuthorization(args.authorization, args.binding, configuration)(args);
5976
+ }
5977
+ async function resolveOutputDocumentAuthorizationWithHook(args, access, resolveHook) {
5720
5978
  const { authorization, binding } = args;
5721
- const { configuration, access } = readServiceAuthorization(authorization, binding);
5722
5979
  const stage = args.stage ?? createDocumentAuthorizationStage(authorization, binding, args.snapshot);
5723
5980
  if (args.stage) assertDocumentAuthorizationStage(stage, authorization, binding, args.snapshot);
5724
5981
  const handle = await resolveDocumentAuthorization(stage, async function(doc, permissions) {
5725
5982
  if (args.owningDocument) return readDocumentAuthorization(args.owningDocument, args.owningBinding).grants;
5726
5983
  if (args.knownOutput) return {};
5727
- const hook = getCapturedAuthorizationOption(configuration, `docPermissions.${access.docPermissionsAccess}`);
5728
- if (typeof hook !== "function") return {};
5984
+ const hook = resolveHook();
5985
+ if (!hook) return {};
5729
5986
  try {
5730
5987
  const run = () => hook.call(this, doc, permissions, args.context);
5731
5988
  const grants = await getRequestWorkState(binding.request, binding.runtime).hookGate.run(run);
5732
- return isPlainObject20(grants) ? grants : {};
5989
+ return isPlainObject21(grants) ? grants : {};
5733
5990
  } catch (error2) {
5734
5991
  warn("docPermissions hook failed; applying empty document permissions", {
5735
5992
  modelName: binding.modelName,
@@ -5746,6 +6003,7 @@ async function resolveOutputDocumentAuthorization(args) {
5746
6003
  return handle;
5747
6004
  }
5748
6005
  function evaluateRequestRule(handle, binding, entry, prefix, access) {
6006
+ assertOwningOutputDocuments(handle);
5749
6007
  const owner = owningDocuments.get(handle);
5750
6008
  if (owner && !entry.path.startsWith(`${(binding.scopePath ?? []).join(".")}.`)) {
5751
6009
  return evaluateRequestRule(owner.handle, owner.binding, entry, prefix, access);
@@ -5754,65 +6012,78 @@ function evaluateRequestRule(handle, binding, entry, prefix, access) {
5754
6012
  handle,
5755
6013
  binding,
5756
6014
  `request:${access}:${entry.path}`,
5757
- function(permissions, grants) {
5758
- return evaluateScopedAccessRule({
5759
- req: this,
5760
- rule: entry.rule,
5761
- globalPermissions: permissions,
5762
- docPermissions: grants,
5763
- modelPermissionPrefix: prefix
5764
- });
6015
+ async function(permissions, grants) {
6016
+ const { authorization } = readDocumentAuthorizationAccess(handle, binding);
6017
+ const classification = requestRuleClassification(entry, authorization, permissions, prefix);
6018
+ let allowed = classification?.decision;
6019
+ if (allowed === void 0) {
6020
+ allowed = await evaluateScopedAccessRule({
6021
+ req: this,
6022
+ rule: entry.rule,
6023
+ globalPermissions: permissions,
6024
+ docPermissions: grants,
6025
+ modelPermissionPrefix: prefix
6026
+ });
6027
+ }
6028
+ assertOwningOutputDocuments(handle);
6029
+ return allowed;
5765
6030
  }
5766
6031
  );
5767
6032
  }
5768
6033
  function evaluateAuthorizedOutputField(handle, binding, field) {
5769
- const { authorization } = readDocumentAuthorization(handle, binding);
5770
- const { configuration, authorization: policy, access } = readServiceAuthorization(authorization, binding);
6034
+ const { authorization } = readDocumentAuthorizationAccess(handle, binding);
6035
+ assertOwningOutputDocuments(handle);
5771
6036
  const path = [...binding.scopePath ?? [], field].join(".");
5772
- const entries = extractOutputFieldRuleEntries(
5773
- configuration.permissionSchema,
5774
- path,
5775
- access.outputAccess
5776
- );
5777
- return memoizeDocumentFieldDecision(handle, binding, `output:${path}`, function(permissions, grants) {
5778
- const args = {
6037
+ const decision = memoizeDocumentFieldDecision(handle, binding, `output:${path}`, async function(permissions) {
6038
+ const policy = callFieldPolicy(authorization, binding);
6039
+ const { outputAccess } = readDocumentAuthorizationAccess(handle, binding);
6040
+ const prepared = prepareOutputPath(policy, authorization, binding, path, outputAccess);
6041
+ const unresolved = unresolvedDocuments.has(handle);
6042
+ const state = unresolved ? prepared.state ?? classifyEffectiveFieldAuthorization({
5779
6043
  path,
5780
- rules: entries.map((entry) => entry.rule),
6044
+ rules: prepared.entries.map((entry) => entry.rule),
6045
+ modelPermissionPrefix: policy.prefix,
5781
6046
  outputFields: policy.outputFields,
5782
- globalPermissions: permissions,
5783
- modelPermissionPrefix: configuration.modelPermissionPrefix ?? ""
5784
- };
5785
- if (unresolvedDocuments.has(handle) && classifyEffectiveFieldAuthorization(args) !== "granted") return false;
5786
- return evaluateEffectiveFieldAuthorization({
5787
- ...args,
5788
- req: this,
5789
- docPermissions: grants,
5790
- evaluateRule: (_rule, index) => evaluateRequestRule(handle, binding, entries[index], args.modelPermissionPrefix, access.outputAccess)
5791
- });
6047
+ globalPermissions: permissions
6048
+ }) : void 0;
6049
+ let allowed;
6050
+ if (unresolved && state !== "granted") allowed = false;
6051
+ else if (prepared.backendGrant) allowed = true;
6052
+ else {
6053
+ allowed = true;
6054
+ for (const entry of prepared.entries) {
6055
+ if (!await evaluateRequestRule(handle, binding, entry, policy.prefix, outputAccess)) {
6056
+ allowed = false;
6057
+ break;
6058
+ }
6059
+ }
6060
+ }
6061
+ assertOwningOutputDocuments(handle);
6062
+ return allowed;
5792
6063
  });
6064
+ return owningDocuments.has(handle) ? decision.then((allowed) => {
6065
+ assertOwningOutputDocuments(handle);
6066
+ return allowed;
6067
+ }) : decision;
5793
6068
  }
5794
6069
  async function requestOutputFieldPaths(handle, binding, access) {
5795
- const { authorization } = readDocumentAuthorization(handle, binding);
5796
- const { configuration } = readServiceAuthorization(authorization, binding);
5797
- const schema = configuration.permissionSchema;
6070
+ const { authorization } = readDocumentAuthorizationAccess(handle, binding);
6071
+ assertOwningOutputDocuments(handle);
6072
+ const policy = callFieldPolicy(authorization, binding);
5798
6073
  const fields = [];
5799
- for (const path of Object.keys(schema ?? {})) {
5800
- const entries = extractOutputFieldRuleEntries(schema, path, access);
6074
+ for (const path of policy.requestPaths ?? Object.keys(policy.schema ?? {})) {
6075
+ const { entries } = prepareOutputPath(policy, authorization, binding, path, access);
5801
6076
  let allowed = true;
5802
6077
  for (const entry of entries) {
5803
- if (!await evaluateRequestRule(
5804
- handle,
5805
- binding,
5806
- entry,
5807
- configuration.modelPermissionPrefix ?? "",
5808
- access
5809
- )) {
6078
+ if (!await evaluateRequestRule(handle, binding, entry, policy.prefix, access)) {
5810
6079
  allowed = false;
5811
6080
  break;
5812
6081
  }
5813
6082
  }
5814
6083
  if (allowed && entries.length) fields.push(path);
5815
6084
  }
6085
+ assertOwningOutputDocuments(handle);
6086
+ readDocumentAuthorizationAccess(handle, binding);
5816
6087
  return fields;
5817
6088
  }
5818
6089
  function authorizeEmbeddedOutputPlan(parent, child) {
@@ -5833,24 +6104,24 @@ function authorizeEmbeddedOutputPlan(parent, child) {
5833
6104
  function hasWholeOutputValueGrant(plan, field) {
5834
6105
  if (plan.fieldStates?.[field] !== "granted") return false;
5835
6106
  const binding = plan.authorizationBinding;
5836
- const { authorization } = readServiceAuthorization(plan.authorization, binding);
5837
- return hasExplicitFieldGrant(authorization.outputFields, [...binding.scopePath ?? [], field].join("."));
6107
+ const { outputFields } = readServiceOutputPolicy(plan.authorization, binding);
6108
+ return hasExplicitFieldGrant(outputFields, [...binding.scopePath ?? [], field].join("."));
5838
6109
  }
5839
6110
  function copyOutputValue(value, plan, path) {
5840
- const { configuration } = readServiceAuthorization(plan.authorization, plan.authorizationBinding);
5841
- const virtualValue = configuration.catalog.virtualPaths?.some((p) => matchesCatalogPattern(p, path));
5842
- const opaque = configuration.catalog.opaquePaths?.some((p) => matchesCatalogPattern(p, path));
6111
+ const { catalog } = readServiceOutputPolicy(plan.authorization, plan.authorizationBinding);
6112
+ const virtualValue = catalog.virtualPaths?.some((p) => matchesCatalogPattern(p, path));
6113
+ const opaque = catalog.opaquePaths?.some((p) => matchesCatalogPattern(p, path));
5843
6114
  const excluded = plan.effectiveSelectNorm.filter((token) => token.startsWith("-")).map((token) => [...plan.authorizationBinding.scopePath ?? [], token.slice(1)].join("."));
5844
6115
  const visit = (input, current) => {
5845
6116
  if (Array.isArray(input)) return input.map((entry) => visit(entry, current));
5846
6117
  if (input instanceof Map) return visit(Object.fromEntries(input), current);
5847
- if (!isPlainObject20(input)) return deepIsolateValue(input);
6118
+ if (!isPlainObject21(input)) return deepIsolateValue(input);
5848
6119
  const output = {};
5849
6120
  for (const [key, entry] of Object.entries(input)) {
5850
6121
  const child = `${current}.${key}`;
5851
6122
  if (excluded.some((entry2) => child === entry2 || child.startsWith(`${entry2}.`))) continue;
5852
6123
  const safeValueKey = !["__proto__", "constructor", "prototype", "_doc", "$__", "__v"].includes(key);
5853
- if (!(opaque ? safeValueKey : isSafeOutputPath(key)) || configuration.catalog.protectedPaths?.some((p) => matchesCatalogPattern(p, child, true)) || !opaque && !virtualValue && !isCatalogOutputPath(configuration.catalog, child))
6124
+ if (!(opaque ? safeValueKey : isSafeOutputPath(key)) || catalog.protectedPaths?.some((p) => matchesCatalogPattern(p, child, true)) || !opaque && !virtualValue && !isCatalogOutputPath(catalog, child))
5854
6125
  continue;
5855
6126
  output[key] = visit(entry, child);
5856
6127
  }
@@ -5860,25 +6131,21 @@ function copyOutputValue(value, plan, path) {
5860
6131
  }
5861
6132
  function copyAuthorizedReference(value, plan, path) {
5862
6133
  if (Array.isArray(value)) return value.map((entry) => copyAuthorizedReference(entry, plan, path));
5863
- if (!isPlainObject20(value)) return deepIsolateValue(value);
6134
+ if (!isPlainObject21(value)) return deepIsolateValue(value);
5864
6135
  const handle = getAssociatedDocumentAuthorization(value, plan.authorization);
5865
6136
  if (handle) {
5866
6137
  const copy = deepIsolateValue(value);
5867
6138
  transferDocumentAuthorizationGraph(value, copy);
5868
6139
  return copy;
5869
6140
  }
5870
- const { relatedTargets } = readServiceAuthorization(plan.authorization, plan.authorizationBinding);
5871
- if (relatedTargets.some((target) => target.path === path && target.kind === "passthrough")) {
5872
- return deepIsolateValue(value);
5873
- }
5874
6141
  return Object.prototype.hasOwnProperty.call(value, "_id") ? deepIsolateValue(value._id) : void 0;
5875
6142
  }
5876
6143
  function retainOutputMetadata(output, source, configuration, scope2) {
5877
6144
  if (scope2.length || configuration.stripPermissionsField) return;
5878
6145
  const parts = (configuration.documentPermissionField ?? "_permissions").split(".");
5879
6146
  let value = source;
5880
- for (const part of parts) value = isPlainObject20(value) ? value[part] : void 0;
5881
- if (!isPlainObject20(value)) return;
6147
+ for (const part of parts) value = isPlainObject21(value) ? value[part] : void 0;
6148
+ if (!isPlainObject21(value)) return;
5882
6149
  const keys2 = configuration.exposedDocPermissionKeys;
5883
6150
  const metadata = Object.fromEntries(
5884
6151
  Object.entries(value).filter(
@@ -5888,7 +6155,7 @@ function retainOutputMetadata(output, source, configuration, scope2) {
5888
6155
  const copy = { ...output };
5889
6156
  let target = copy;
5890
6157
  for (const part of parts.slice(0, -1)) {
5891
- target[part] = isPlainObject20(target[part]) ? { ...target[part] } : {};
6158
+ target[part] = isPlainObject21(target[part]) ? { ...target[part] } : {};
5892
6159
  target = target[part];
5893
6160
  }
5894
6161
  target[parts[parts.length - 1]] = deepIsolateValue(metadata);
@@ -5930,11 +6197,11 @@ async function filterExistingOutput(source, plan, handle, associated) {
5930
6197
  for (const entry of value2) values.push(await one(entry) ?? null);
5931
6198
  return values;
5932
6199
  }
5933
- if (!isPlainObject20(value2)) return value2 === null ? null : void 0;
6200
+ if (!isPlainObject21(value2)) return value2 === null ? null : void 0;
5934
6201
  const childHandle = associated ? getAssociatedDocumentAuthorization(value2) : void 0;
5935
6202
  let child = scoped;
5936
6203
  if (childHandle) {
5937
- const state = readDocumentAuthorization(childHandle, scoped.authorizationBinding);
6204
+ const state = readDocumentAuthorizationAccess(childHandle, scoped.authorizationBinding);
5938
6205
  if (getAuthorizationProvenance(state.authorization) !== getAuthorizationProvenance(plan.authorization))
5939
6206
  authorizationError("child call mismatch");
5940
6207
  child = helperPlan(state.authorization, scoped.authorizationBinding);
@@ -5998,7 +6265,7 @@ async function filterModelOutputForHelper(args) {
5998
6265
  if (normalized.kind === "document") {
5999
6266
  const { documentAuthorization: handle2, binding: binding2 } = normalized;
6000
6267
  assertDocumentAuthorization(handle2, { binding: binding2, output: args.doc, outputAccess: access });
6001
- const state = readDocumentAuthorization(handle2, binding2);
6268
+ const state = readDocumentAuthorizationAccess(handle2, binding2);
6002
6269
  return filterExistingOutput(
6003
6270
  args.doc,
6004
6271
  helperPlan(state.authorization, binding2),
@@ -6010,7 +6277,7 @@ async function filterModelOutputForHelper(args) {
6010
6277
  const plan = helperPlan(normalized.authorization, binding);
6011
6278
  if (args.doc == null) return { output: args.doc, fields: [] };
6012
6279
  const source = isolateDocForOutput(args.doc);
6013
- if (!isPlainObject20(source)) return authorizationError("output helper requires one plain or hydrated document");
6280
+ if (!isPlainObject21(source)) return authorizationError("output helper requires one plain or hydrated document");
6014
6281
  const knownOutput = !!args.doc && typeof args.doc === "object" && isKnownAuthorizationOutput(args.doc);
6015
6282
  const handle = await resolveOutputDocumentAuthorization({
6016
6283
  authorization: normalized.authorization,
@@ -6021,7 +6288,7 @@ async function filterModelOutputForHelper(args) {
6021
6288
  });
6022
6289
  return filterExistingOutput(source, plan, handle, false);
6023
6290
  }
6024
- var unresolvedDocuments, owningDocuments;
6291
+ var unresolvedDocuments, owningDocuments, fieldPolicies;
6025
6292
  var init_output_field_filter = __esm({
6026
6293
  "src/acl/output-field-filter.ts"() {
6027
6294
  init_permissions_alias();
@@ -6037,6 +6304,7 @@ var init_output_field_filter = __esm({
6037
6304
  init_output_projection();
6038
6305
  unresolvedDocuments = /* @__PURE__ */ new WeakSet();
6039
6306
  owningDocuments = /* @__PURE__ */ new WeakMap();
6307
+ fieldPolicies = /* @__PURE__ */ new WeakMap();
6040
6308
  }
6041
6309
  });
6042
6310
 
@@ -6105,28 +6373,54 @@ __export(finalize_model_output_exports, {
6105
6373
  finalizeModelOutput: () => finalizeModelOutput,
6106
6374
  finalizeModelOutputs: () => finalizeModelOutputs
6107
6375
  });
6108
- import { get as get9, isPlainObject as isPlainObject21, set as set7 } from "@web-ts-toolkit/utils";
6109
- async function finalizeSingleDoc(args, input) {
6110
- const scopePath = args.scopePath;
6376
+ import { get as get9, isPlainObject as isPlainObject22, set as set7 } from "@web-ts-toolkit/utils";
6377
+ function assertFinalizerCall(args, prepared) {
6111
6378
  const plan = args.plan;
6112
6379
  const binding = plan.authorizationBinding;
6113
- const captured = plan.authorization && binding ? readServiceAuthorization(plan.authorization, binding) : void 0;
6114
- if (plan.authorization && (!binding || binding.request !== args.request || binding.modelName !== args.receivingModelName || captured?.access.virtualAccess !== args.virtualAccess || captured.access.outputAccess !== args.outputAccess || captured.access.docPermissionsAccess !== args.docPermissionsAccess || (binding.scopePath ?? []).join(".") !== scopePath.filter((p) => p !== "sub").join("."))) {
6380
+ if (plan.authorization !== prepared.authorization) authorizationError("finalizer call changed during pass");
6381
+ if (plan.authorization && binding) assertAuthorizationBinding(plan.authorization, binding);
6382
+ const access = prepared.policy?.access;
6383
+ if (plan.authorization && (!binding || binding.request !== args.request || binding.modelName !== args.receivingModelName || access?.virtualAccess !== args.virtualAccess || access.outputAccess !== args.outputAccess || access.docPermissionsAccess !== args.docPermissionsAccess || (binding.scopePath ?? []).join(".") !== args.scopePath.filter((p) => p !== "sub").join("."))) {
6115
6384
  authorizationError("finalizer call/model/scope/access mismatch");
6116
6385
  }
6117
- const snapshot = captured ? captured.configuration : args.snapshot;
6386
+ }
6387
+ function prepareFinalizer(args, sharedContext = false) {
6388
+ const { authorization, authorizationBinding: binding } = args.plan;
6389
+ const policy = authorization && binding ? readServiceOutputPolicy(authorization, binding) : void 0;
6390
+ assertFinalizerCall(args, { authorization, policy });
6391
+ const configuration = authorization ? readServiceFinalizerConfiguration(authorization, binding) : void 0;
6392
+ const snapshot = configuration ? configuration : deepIsolateValue(args.snapshot);
6393
+ return {
6394
+ authorization,
6395
+ policy,
6396
+ snapshot,
6397
+ baseContext: deepIsolateValue(args.context ?? {}),
6398
+ sharedContext,
6399
+ scopeVirtualNames: getScopeVirtualNames(snapshot, args.scopePath),
6400
+ scopedSchema: configuration ? null : getScopedPermissionSchema2(snapshot.permissionSchema, args.scopePath),
6401
+ permissions: authorization ? getAuthorizationPermissions(authorization, binding) : void 0,
6402
+ resolveDocument: authorization ? prepareOutputDocumentAuthorization(authorization, binding, configuration) : void 0
6403
+ };
6404
+ }
6405
+ async function finalizeSingleDoc(args, input, prepared) {
6406
+ assertFinalizerCall(args, prepared);
6407
+ const scopePath = args.scopePath;
6408
+ const plan = args.plan;
6409
+ const binding = plan.authorizationBinding;
6410
+ const captured = prepared.policy;
6411
+ const snapshot = prepared.snapshot;
6118
6412
  const limit = args.resolvedLimit;
6119
6413
  const gate = args.sharedGate;
6120
6414
  const original = getIncludeOriginal(input);
6121
6415
  const privateRow = args.virtualPopulateRow ?? (original && typeof original === "object" ? getVirtualPopulateRow(original) : void 0);
6122
6416
  const privateAddress = args.virtualPopulateAddress ?? [];
6123
6417
  const isolated = isolateDocForOutput(input);
6124
- const output = isPlainObject21(isolated) ? isolated : {};
6125
- if (captured && isPlainObject21(input))
6418
+ const output = isPlainObject22(isolated) ? isolated : {};
6419
+ if (captured && isPlainObject22(input))
6126
6420
  for (const key of Object.keys(output)) {
6127
6421
  transferDocumentAuthorizationGraph(input[key], output[key]);
6128
6422
  }
6129
- const baseContext = args.context ?? {};
6423
+ const baseContext = prepared.sharedContext ? deepIsolateValue(prepared.baseContext) : prepared.baseContext;
6130
6424
  const operation = captured?.access.operation ?? baseContext.operation ?? args.operation ?? args.virtualAccess;
6131
6425
  const scopeContext = {
6132
6426
  ...baseContext,
@@ -6144,7 +6438,7 @@ async function finalizeSingleDoc(args, input) {
6144
6438
  if (captured) {
6145
6439
  const projected = args.documentAuthorizationStage ? input : deepIsolateValue(output);
6146
6440
  metadataSource = deepIsolateValue(output);
6147
- documentAuthorization = await resolveOutputDocumentAuthorization({
6441
+ documentAuthorization = await prepared.resolveDocument({
6148
6442
  authorization: plan.authorization,
6149
6443
  binding,
6150
6444
  snapshot: projected,
@@ -6157,13 +6451,10 @@ async function finalizeSingleDoc(args, input) {
6157
6451
  }
6158
6452
  const docGrants = documentAuthorization ? readDocumentAuthorization(documentAuthorization, binding).grants : await resolveDocPermissions(args, output, scopeContext);
6159
6453
  scopeContext.docPermissions = deepIsolateValue(docGrants);
6160
- const globalPerms = captured ? getAuthorizationPermissions(plan.authorization, binding) : resolveGlobalPermissions(args);
6161
- const modelPermissionPrefix = snapshot.modelPermissionPrefix ?? "";
6162
- const documentPermissionField = snapshot.documentPermissionField ?? "_permissions";
6163
- const scopedSchema = getScopedPermissionSchema2(
6164
- snapshot.permissionSchema ?? null,
6165
- scopePath
6166
- );
6454
+ const globalPerms = prepared.permissions ?? resolveGlobalPermissions(args);
6455
+ const modelPermissionPrefix = captured ? "" : deepIsolateValue(snapshot.modelPermissionPrefix ?? "");
6456
+ const documentPermissionField = captured ? "_permissions" : deepIsolateValue(snapshot.documentPermissionField ?? "_permissions");
6457
+ const scopedSchema = deepIsolateValue(prepared.scopedSchema);
6167
6458
  const childPlans = plan.childPlans ?? {};
6168
6459
  for (const [containerName, childPlan] of Object.entries(childPlans)) {
6169
6460
  if (!(containerName in output)) continue;
@@ -6177,17 +6468,18 @@ async function finalizeSingleDoc(args, input) {
6177
6468
  );
6178
6469
  continue;
6179
6470
  }
6180
- if (captured && !isPlainObject21(rawContainer) && !Array.isArray(rawContainer)) {
6471
+ if (captured && !isPlainObject22(rawContainer) && !Array.isArray(rawContainer)) {
6181
6472
  delete output[containerName];
6182
6473
  continue;
6183
6474
  }
6184
6475
  retainRawAssociation(output, containerName, rawContainer);
6185
- const childSnapshot = snapshot;
6186
6476
  const childBase = {
6187
6477
  ...args,
6188
6478
  scopePath: [...childPlan.scopePath ?? [...scopePath, containerName, "sub"]],
6189
6479
  plan: captured ? authorizeEmbeddedOutputPlan(plan, childPlan) : childPlan,
6190
- snapshot: childSnapshot,
6480
+ // Authenticated preparation reads its own authority. Do not clone an
6481
+ // otherwise unused public configuration graph on default traversal.
6482
+ snapshot: args.snapshot,
6191
6483
  // Embedded reuses the owning parent's grants + accesses (VIRT-00A D2).
6192
6484
  docPermissions: docGrants,
6193
6485
  context: scopeContext,
@@ -6198,10 +6490,11 @@ async function finalizeSingleDoc(args, input) {
6198
6490
  related: [],
6199
6491
  virtualPopulateRow: privateRow
6200
6492
  };
6493
+ let childPreparation;
6201
6494
  const finalizeOneChild = async (rawChild, address) => {
6202
6495
  if (captured && Array.isArray(rawChild))
6203
6496
  return mapWithConcurrencyLimit(rawChild, limit, (entry, index) => finalizeOneChild(entry, [...address, index]));
6204
- if (!isPlainObject21(rawChild) && !Array.isArray(rawChild)) return captured ? null : deepIsolateValue(rawChild);
6497
+ if (!isPlainObject22(rawChild) && !Array.isArray(rawChild)) return captured ? null : deepIsolateValue(rawChild);
6205
6498
  if (args.traversal?.finalizeEmbeddedChildren) {
6206
6499
  return args.traversal.finalizeEmbeddedChildren({
6207
6500
  containerName,
@@ -6218,7 +6511,14 @@ async function finalizeSingleDoc(args, input) {
6218
6511
  } : { ...args, virtualPopulateRow: privateRow, virtualPopulateAddress: address }
6219
6512
  });
6220
6513
  }
6221
- return finalizeSingleDoc({ ...childBase, virtualPopulateAddress: address }, rawChild);
6514
+ return finalizeSingleDoc(
6515
+ { ...childBase, virtualPopulateAddress: address },
6516
+ rawChild,
6517
+ childPreparation ??= prepareFinalizer(
6518
+ childBase,
6519
+ Array.isArray(rawContainer) && (rawContainer.length > 1 || Array.isArray(rawContainer[0]))
6520
+ )
6521
+ );
6222
6522
  };
6223
6523
  if (Array.isArray(rawContainer)) {
6224
6524
  const finalized = await mapWithConcurrencyLimit(
@@ -6227,7 +6527,7 @@ async function finalizeSingleDoc(args, input) {
6227
6527
  (entry, index) => finalizeOneChild(entry, [...privateAddress, containerName, index])
6228
6528
  );
6229
6529
  output[containerName] = finalized;
6230
- } else if (isPlainObject21(rawContainer)) {
6530
+ } else if (isPlainObject22(rawContainer)) {
6231
6531
  output[containerName] = await finalizeOneChild(rawContainer, [
6232
6532
  ...privateAddress,
6233
6533
  containerName
@@ -6270,8 +6570,9 @@ async function finalizeSingleDoc(args, input) {
6270
6570
  context: scopeContext,
6271
6571
  related: target.plan.relatedPlans?.length ? (args.related ?? []).filter((entry) => entry.path.startsWith(`${target.path}.`)) : []
6272
6572
  };
6573
+ let relatedPreparation;
6273
6574
  const finalizeOneRelated = async (rawChild, originalChild = rawChild) => {
6274
- if (!isPlainObject21(rawChild) && !Array.isArray(rawChild)) return rawChild;
6575
+ if (!isPlainObject22(rawChild) && !Array.isArray(rawChild)) return rawChild;
6275
6576
  if (typeof rawChild?._bsontype === "string") return rawChild;
6276
6577
  const childOriginal = getIncludeOriginal(originalChild);
6277
6578
  const childRow = childOriginal && typeof childOriginal === "object" ? getVirtualPopulateRow(childOriginal) : void 0;
@@ -6283,7 +6584,11 @@ async function finalizeSingleDoc(args, input) {
6283
6584
  parentArgs: { ...args, virtualPopulateRow: childRow, virtualPopulateAddress: [] }
6284
6585
  });
6285
6586
  }
6286
- return finalizeSingleDoc({ ...relatedBase, virtualPopulateRow: childRow }, rawChild);
6587
+ return finalizeSingleDoc(
6588
+ { ...relatedBase, virtualPopulateRow: childRow },
6589
+ rawChild,
6590
+ relatedPreparation ??= prepareFinalizer(relatedBase, Array.isArray(rawRelated) && rawRelated.length > 1)
6591
+ );
6287
6592
  };
6288
6593
  let finalized;
6289
6594
  if (Array.isArray(rawRelated)) {
@@ -6292,7 +6597,7 @@ async function finalizeSingleDoc(args, input) {
6292
6597
  limit,
6293
6598
  (entry, index) => finalizeOneRelated(entry, Array.isArray(originalRelated) ? originalRelated[index] : void 0)
6294
6599
  );
6295
- } else if (isPlainObject21(rawRelated)) {
6600
+ } else if (isPlainObject22(rawRelated)) {
6296
6601
  finalized = await finalizeOneRelated(rawRelated, originalRelated);
6297
6602
  } else {
6298
6603
  continue;
@@ -6303,18 +6608,24 @@ async function finalizeSingleDoc(args, input) {
6303
6608
  } catch {
6304
6609
  }
6305
6610
  }
6306
- const scopeVirtualNames = getScopeVirtualNames(snapshot, scopePath);
6307
- for (const virtualName of scopeVirtualNames) {
6611
+ for (const virtualName of prepared.scopeVirtualNames) {
6308
6612
  if (virtualName in output) delete output[virtualName];
6309
6613
  }
6310
- const stableBase = deepIsolateValue(output);
6311
- const scopeLabel = scopePath.length > 0 ? scopePath.join(".") : "";
6312
6614
  const candidates = plan.candidates ?? [];
6615
+ const needsGetterView = candidates.some((candidate) => {
6616
+ const descriptor = candidate.descriptor;
6617
+ if (typeof descriptor?.get !== "function") return false;
6618
+ if (!privateRow && descriptor.dependsOnPopulate?.length) return false;
6619
+ const dependsOn = Array.isArray(descriptor.dependsOn) ? descriptor.dependsOn : [];
6620
+ return dependsOn.every((dep) => typeof dep === "string" && !isAbsentDependency(output, dep));
6621
+ });
6622
+ let stableBase = needsGetterView ? deepIsolateValue(output) : void 0;
6623
+ const scopeLabel = scopePath.length > 0 ? scopePath.join(".") : "";
6313
6624
  const staged = /* @__PURE__ */ new Map();
6314
6625
  const runCandidate = async (candidate) => {
6315
6626
  const dependsOn = Array.isArray(candidate.descriptor?.dependsOn) ? candidate.descriptor.dependsOn : [];
6316
6627
  for (const dep of dependsOn) {
6317
- if (typeof dep !== "string" || isAbsentDependency(stableBase, dep)) return;
6628
+ if (typeof dep !== "string" || isAbsentDependency(stableBase ?? output, dep)) return;
6318
6629
  }
6319
6630
  const allowed = documentAuthorization ? await evaluateAuthorizedOutputField(documentAuthorization, binding, candidate.name) : await evaluateEffectiveFieldAuthorization({
6320
6631
  req: args.request,
@@ -6329,9 +6640,9 @@ async function finalizeSingleDoc(args, input) {
6329
6640
  if (typeof getter !== "function") return;
6330
6641
  const privateInputs = privateRow ? readVirtualPopulateInputs(privateRow, privateAddress, candidate.name) : candidate.descriptor.dependsOnPopulate?.length ? { available: false } : { available: true, populated: {} };
6331
6642
  if (!privateInputs.available) return;
6332
- const isolatedView = deepIsolateValue(stableBase);
6643
+ const isolatedView = deepIsolateValue(stableBase ??= deepIsolateValue(output));
6333
6644
  const virtualContext = {
6334
- ...scopeContext,
6645
+ ...deepIsolateValue(scopeContext),
6335
6646
  docPermissions: deepIsolateValue(docGrants)
6336
6647
  };
6337
6648
  const outcome = await runVirtualGetter({
@@ -6412,7 +6723,6 @@ async function finalizeSingleDoc(args, input) {
6412
6723
  if (!captured && documentPermissionField in output) keep.add(documentPermissionField);
6413
6724
  const trimmed = {};
6414
6725
  for (const key of keep) {
6415
- if (privateRow && !await isVirtualPopulateReferenceVisible(privateRow, privateAddress, key)) continue;
6416
6726
  if (privateRow && !hasVirtualPopulatePublicReference(privateRow, privateAddress, key)) {
6417
6727
  const ref = readVirtualPopulateOriginalReference(privateRow, privateAddress, key);
6418
6728
  if (ref) {
@@ -6423,12 +6733,25 @@ async function finalizeSingleDoc(args, input) {
6423
6733
  if (!(key in output)) continue;
6424
6734
  if (captured && !plan.childPlans[key]) {
6425
6735
  const path = [...binding.scopePath ?? [], key].join(".");
6426
- trimmed[key] = captured.configuration.catalog.referencePaths?.some((p) => matchesCatalogPattern(p, path)) ? copyAuthorizedReference(output[key], plan, path) : copyOutputValue(output[key], plan, path);
6736
+ trimmed[key] = captured.catalog.referencePaths?.some((p) => matchesCatalogPattern(p, path)) ? copyAuthorizedReference(output[key], plan, path) : copyOutputValue(output[key], plan, path);
6427
6737
  if (trimmed[key] === void 0) delete trimmed[key];
6428
6738
  } else trimmed[key] = output[key];
6429
6739
  }
6430
6740
  if (documentAuthorization) {
6431
- retainOutputMetadata(trimmed, metadataSource, captured.configuration, binding.scopePath ?? []);
6741
+ retainOutputMetadata(
6742
+ trimmed,
6743
+ metadataSource,
6744
+ copyAuthorizationValue(
6745
+ {
6746
+ documentPermissionField: snapshot.documentPermissionField,
6747
+ stripPermissionsField: snapshot.stripPermissionsField,
6748
+ exposedDocPermissionKeys: snapshot.exposedDocPermissionKeys,
6749
+ disableFieldPermissions: snapshot.disableFieldPermissions
6750
+ },
6751
+ true
6752
+ ),
6753
+ binding.scopePath ?? []
6754
+ );
6432
6755
  associateDocumentAuthorization(documentAuthorization, binding, trimmed);
6433
6756
  args.onFinalized?.(
6434
6757
  trimmed,
@@ -6437,7 +6760,7 @@ async function finalizeSingleDoc(args, input) {
6437
6760
  output: trimmed,
6438
6761
  outputAccess: args.outputAccess,
6439
6762
  authorization: plan.authorization,
6440
- stage: readDocumentAuthorization(documentAuthorization, binding).stage
6763
+ stage: readDocumentAuthorizationAccess(documentAuthorization, binding).stage
6441
6764
  })
6442
6765
  );
6443
6766
  }
@@ -6458,7 +6781,7 @@ async function finalizeModelOutput(args) {
6458
6781
  sharedGate: gate,
6459
6782
  resolvedLimit: limit
6460
6783
  };
6461
- return finalizeSingleDoc(internal, args.input);
6784
+ return finalizeSingleDoc(internal, args.input, prepareFinalizer(internal));
6462
6785
  }
6463
6786
  async function finalizeModelOutputs(args) {
6464
6787
  const gate = finalizerGate(args);
@@ -6467,16 +6790,24 @@ async function finalizeModelOutputs(args) {
6467
6790
  if ("documentAuthorizationStage" in args || documentAuthorizationStages && (documentAuthorizationStages.length !== inputs.length || new Set(documentAuthorizationStages).size !== inputs.length)) {
6468
6791
  authorizationError("list finalization requires a distinct document stage per row");
6469
6792
  }
6793
+ if (!inputs.length) return [];
6794
+ const internal = {
6795
+ ...rest,
6796
+ scopePath: [...rest.scopePath ?? rest.plan.scopePath ?? []],
6797
+ sharedGate: gate,
6798
+ resolvedLimit: limit
6799
+ };
6800
+ const prepared = prepareFinalizer(internal, inputs.length > 1);
6470
6801
  return mapWithConcurrencyLimit(inputs, limit, (input, index) => {
6471
- const internal = {
6472
- ...rest,
6473
- scopePath: [...rest.scopePath ?? rest.plan.scopePath ?? []],
6474
- onFinalized: rest.onFinalized ? (output, context) => rest.onFinalized(output, context, index) : void 0,
6475
- documentAuthorizationStage: documentAuthorizationStages?.[index],
6476
- sharedGate: gate,
6477
- resolvedLimit: limit
6478
- };
6479
- return finalizeSingleDoc(internal, input);
6802
+ return finalizeSingleDoc(
6803
+ {
6804
+ ...internal,
6805
+ onFinalized: rest.onFinalized ? (output, context) => rest.onFinalized(output, context, index) : void 0,
6806
+ documentAuthorizationStage: documentAuthorizationStages?.[index]
6807
+ },
6808
+ input,
6809
+ prepared
6810
+ );
6480
6811
  });
6481
6812
  }
6482
6813
  var isVirtualContainerValue3, getScopeVirtualsObject2, getScopeVirtualNames, getScopedPermissionSchema2, resolveGlobalPermissions, resolveDocPermissions, isAbsentDependency, __finalizeInternals;
@@ -6494,20 +6825,20 @@ var init_finalize_model_output = __esm({
6494
6825
  init_field_catalog();
6495
6826
  init_virtual_populate_execution();
6496
6827
  init_include_output();
6497
- isVirtualContainerValue3 = (value) => isPlainObject21(value) && "sub" in value;
6828
+ isVirtualContainerValue3 = (value) => isPlainObject22(value) && "sub" in value;
6498
6829
  getScopeVirtualsObject2 = (rootVirtuals, scopePath) => {
6499
- if (!rootVirtuals || !isPlainObject21(rootVirtuals)) return null;
6830
+ if (!rootVirtuals || !isPlainObject22(rootVirtuals)) return null;
6500
6831
  if (scopePath.length === 0) return rootVirtuals;
6501
6832
  let current = rootVirtuals;
6502
6833
  for (let i = 0; i < scopePath.length; i += 2) {
6503
6834
  const field = scopePath[i];
6504
6835
  const subMarker = scopePath[i + 1];
6505
6836
  if (!field || subMarker !== "sub") return null;
6506
- if (!isPlainObject21(current)) return null;
6837
+ if (!isPlainObject22(current)) return null;
6507
6838
  const container = current[field];
6508
6839
  if (!isVirtualContainerValue3(container)) return null;
6509
6840
  current = container.sub;
6510
- if (!isPlainObject21(current)) return null;
6841
+ if (!isPlainObject22(current)) return null;
6511
6842
  }
6512
6843
  return current;
6513
6844
  };
@@ -6520,11 +6851,11 @@ var init_finalize_model_output = __esm({
6520
6851
  return Object.entries(scopeVirtuals).filter(([, v]) => v !== void 0 && !isVirtualContainerValue3(v)).map(([k]) => k);
6521
6852
  };
6522
6853
  getScopedPermissionSchema2 = (root, scopePath) => {
6523
- if (!root || !isPlainObject21(root)) return null;
6854
+ if (!root || !isPlainObject22(root)) return null;
6524
6855
  if (scopePath.length === 0) return root;
6525
6856
  try {
6526
6857
  const scoped = get9(root, scopePath.join("."));
6527
- return isPlainObject21(scoped) ? scoped : null;
6858
+ return isPlainObject22(scoped) ? scoped : null;
6528
6859
  } catch {
6529
6860
  return null;
6530
6861
  }
@@ -6545,7 +6876,7 @@ var init_finalize_model_output = __esm({
6545
6876
  };
6546
6877
  resolveDocPermissions = async (args, isolatedForHook, scopeContext) => {
6547
6878
  if (args.docPermissions != null && typeof args.docPermissions === "object" && !Array.isArray(args.docPermissions)) {
6548
- const grants = args.docPermissions;
6879
+ const grants = deepIsolateValue(args.docPermissions);
6549
6880
  scopeContext.docPermissions = grants;
6550
6881
  return grants;
6551
6882
  }
@@ -6583,7 +6914,7 @@ var init_finalize_model_output = __esm({
6583
6914
  const grants = await args.sharedGate.run(
6584
6915
  () => hook.call(args.request, isolatedForHook, globalPerms, scopeContext)
6585
6916
  );
6586
- const normalized = isPlainObject21(grants) ? grants : {};
6917
+ const normalized = isPlainObject22(grants) ? grants : {};
6587
6918
  scopeContext.docPermissions = normalized;
6588
6919
  return normalized;
6589
6920
  }
@@ -6605,19 +6936,21 @@ var init_finalize_model_output = __esm({
6605
6936
  });
6606
6937
 
6607
6938
  // src/index.ts
6608
- import { isPlainObject as isPlainObject30, isString as isString10, isUndefined as isUndefined4 } from "@web-ts-toolkit/utils";
6939
+ import { isPlainObject as isPlainObject31, isString as isString10, isUndefined as isUndefined4 } from "@web-ts-toolkit/utils";
6609
6940
 
6610
6941
  // src/middleware.ts
6611
6942
  import JsonRouter5 from "@web-ts-toolkit/express-json-router";
6612
- import { isArray as isArray13, isFunction as isFunction7, isPlainObject as isPlainObject26, isString as isString7 } from "@web-ts-toolkit/utils";
6943
+ import { isArray as isArray13, isFunction as isFunction7, isPlainObject as isPlainObject27, isString as isString7 } from "@web-ts-toolkit/utils";
6613
6944
 
6614
6945
  // src/core.ts
6615
6946
  init_public_admission();
6616
6947
  init_options();
6948
+ init_permission();
6617
6949
  import {
6618
6950
  castArray as castArray4,
6619
6951
  compact as compact3,
6620
6952
  forEach as forEach6,
6953
+ get as get15,
6621
6954
  isArray as isArray12,
6622
6955
  isBoolean as isBoolean5,
6623
6956
  isFunction as isFunction6,
@@ -6627,27 +6960,6 @@ import {
6627
6960
  uniq as uniq3
6628
6961
  } from "@web-ts-toolkit/utils";
6629
6962
 
6630
- // src/meta.ts
6631
- init_runtime();
6632
- init_runtime_context();
6633
- var getRuntime5 = () => getActiveRuntime() ?? defaultRuntime;
6634
- var getModelRef = (modelName, refPath) => {
6635
- const direct = getRuntime5().getModelRef(modelName, refPath);
6636
- if (direct) return direct;
6637
- if (!refPath.includes(".")) return null;
6638
- try {
6639
- const runtime = getRuntime5();
6640
- const instance = runtime.getModelInstance?.(
6641
- modelName
6642
- );
6643
- const schemaPath = instance?.schema?.path?.(refPath);
6644
- const ref = schemaPath?.options?.ref ?? schemaPath?.caster?.options?.ref ?? null;
6645
- return typeof ref === "string" && ref.length > 0 ? ref : null;
6646
- } catch {
6647
- return null;
6648
- }
6649
- };
6650
-
6651
6963
  // src/services/base.ts
6652
6964
  init_options();
6653
6965
  init_helpers();
@@ -6663,7 +6975,7 @@ import {
6663
6975
  forEach as forEach3,
6664
6976
  get as get8,
6665
6977
  isArray as isArray9,
6666
- isPlainObject as isPlainObject17,
6978
+ isPlainObject as isPlainObject18,
6667
6979
  set as set6,
6668
6980
  uniq
6669
6981
  } from "@web-ts-toolkit/utils";
@@ -6788,7 +7100,7 @@ init_public_admission();
6788
7100
  // src/correlated-includes.ts
6789
7101
  init_request_complexity();
6790
7102
  init_sort_policy();
6791
- import { cloneDeep, isArray as isArray6, isPlainObject as isPlainObject14 } from "@web-ts-toolkit/utils";
7103
+ import { cloneDeep, isArray as isArray6, isPlainObject as isPlainObject15 } from "@web-ts-toolkit/utils";
6792
7104
  var PARENT_MARKER_KEY = "$parent";
6793
7105
  var ESCAPE_WRAPPER_KEY = "$escape";
6794
7106
  var SUBQUERY_PAYLOAD_KEYS = /* @__PURE__ */ new Set(["$$sq", "$$date"]);
@@ -6799,10 +7111,10 @@ var FIELD_SEGMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_$]*$/;
6799
7111
  var ARRAY_INDEX_SEGMENT_PATTERN = /^(0|[1-9]\d*)$/;
6800
7112
  var ownKeys = (value) => Object.keys(value);
6801
7113
  function isParentRefMarker(value) {
6802
- return isPlainObject14(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === PARENT_MARKER_KEY;
7114
+ return isPlainObject15(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === PARENT_MARKER_KEY;
6803
7115
  }
6804
7116
  function isEscapedParentRef(value) {
6805
- return isPlainObject14(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === ESCAPE_WRAPPER_KEY;
7117
+ return isPlainObject15(value) && ownKeys(value).length === 1 && ownKeys(value)[0] === ESCAPE_WRAPPER_KEY;
6806
7118
  }
6807
7119
  function hasMarkerKey(value) {
6808
7120
  return ownKeys(value).includes(PARENT_MARKER_KEY) || ownKeys(value).includes(ESCAPE_WRAPPER_KEY);
@@ -6826,7 +7138,7 @@ function containsParentMarkerShape(value) {
6826
7138
  seen.add(current);
6827
7139
  return current.some(visit);
6828
7140
  }
6829
- if (isPlainObject14(current)) {
7141
+ if (isPlainObject15(current)) {
6830
7142
  if (seen.has(current)) return false;
6831
7143
  seen.add(current);
6832
7144
  if (hasMarkerKey(current)) return true;
@@ -6856,7 +7168,7 @@ function scanTemplateValueNode(value, path, issues) {
6856
7168
  }
6857
7169
  return;
6858
7170
  }
6859
- if (isPlainObject14(value)) {
7171
+ if (isPlainObject15(value)) {
6860
7172
  if (hasMarkerKey(value)) {
6861
7173
  issues.push({
6862
7174
  message: 'Malformed $parent reference: objects carrying $parent must be exactly { "$parent": "<field>" }',
@@ -6921,7 +7233,7 @@ function scanTemplateObjectEntries(obj, basePath, issues) {
6921
7233
  });
6922
7234
  return;
6923
7235
  }
6924
- if (isPlainObject14(clause)) {
7236
+ if (isPlainObject15(clause)) {
6925
7237
  scanTemplateObjectEntries(clause, clausePath, issues);
6926
7238
  }
6927
7239
  });
@@ -6932,7 +7244,7 @@ function scanTemplateObjectEntries(obj, basePath, issues) {
6932
7244
  }
6933
7245
  function validateCorrelatedFilterTemplate(filter2) {
6934
7246
  const issues = [];
6935
- if (!isPlainObject14(filter2)) {
7247
+ if (!isPlainObject15(filter2)) {
6936
7248
  issues.push({ message: "Correlated include filter must be an object", path: [] });
6937
7249
  return issues;
6938
7250
  }
@@ -6981,7 +7293,7 @@ function collectFilterReferencePaths(filter2) {
6981
7293
  value.forEach(visitValue);
6982
7294
  return;
6983
7295
  }
6984
- if (isPlainObject14(value)) {
7296
+ if (isPlainObject15(value)) {
6985
7297
  if (seen.has(value)) return;
6986
7298
  seen.add(value);
6987
7299
  for (const [key, child] of Object.entries(value)) {
@@ -7027,7 +7339,7 @@ function getOwnPathValue(parent, path) {
7027
7339
  return current;
7028
7340
  }
7029
7341
  function cloneSubstitutedValue(value) {
7030
- if (isPlainObject14(value) || isArray6(value)) return cloneDeep(value);
7342
+ if (isPlainObject15(value) || isArray6(value)) return cloneDeep(value);
7031
7343
  return value;
7032
7344
  }
7033
7345
  function literalParentRefObject(path) {
@@ -7076,7 +7388,7 @@ function resolveObjectEntries(obj, parent) {
7076
7388
  if (isParentRefMarker(clause) || isEscapedParentRef(clause)) {
7077
7389
  throw new CorrelatedReferenceError(`Unsupported $parent reference as a direct ${key} clause`);
7078
7390
  }
7079
- if (!isPlainObject14(clause)) return cloneSubstitutedValue(clause);
7391
+ if (!isPlainObject15(clause)) return cloneSubstitutedValue(clause);
7080
7392
  return resolveObjectEntries(clause, parent);
7081
7393
  });
7082
7394
  continue;
@@ -7105,10 +7417,10 @@ function resolveValueNode(template, parent, bare) {
7105
7417
  }
7106
7418
  const value = getOwnPathValue(parent, path);
7107
7419
  if (value === void 0 || value === null) throw UNRESOLVABLE;
7108
- if (bare && isPlainObject14(value)) return { $eq: cloneDeep(value) };
7420
+ if (bare && isPlainObject15(value)) return { $eq: cloneDeep(value) };
7109
7421
  return cloneSubstitutedValue(value);
7110
7422
  }
7111
- if (isPlainObject14(template)) {
7423
+ if (isPlainObject15(template)) {
7112
7424
  if (hasMarkerKey(template)) {
7113
7425
  throw new CorrelatedReferenceError(
7114
7426
  'Malformed $parent reference: objects carrying $parent must be exactly { "$parent": "<field>" }'
@@ -7150,7 +7462,7 @@ function resolveCorrelatedIdTemplate(id, parent) {
7150
7462
  }
7151
7463
  function resolveCorrelatedFilterTemplate(filter2, parent) {
7152
7464
  try {
7153
- if (!isPlainObject14(filter2) || isParentRefMarker(filter2) || isEscapedParentRef(filter2)) {
7465
+ if (!isPlainObject15(filter2) || isParentRefMarker(filter2) || isEscapedParentRef(filter2)) {
7154
7466
  throw new CorrelatedReferenceError("Correlated include filter must be an object");
7155
7467
  }
7156
7468
  return { status: "resolved", filter: resolveObjectEntries(filter2, parent) };
@@ -7165,7 +7477,7 @@ function validateExpandedCorrelatedOperands(expanded, limits) {
7165
7477
  var MAX_NESTED_INCLUDE_DEPTH = 10;
7166
7478
  function validateCorrelatedArgsShape(args, op, depth) {
7167
7479
  if (args === void 0) return [];
7168
- if (!isPlainObject14(args)) return ["Correlated include args must be an object"];
7480
+ if (!isPlainObject15(args)) return ["Correlated include args must be an object"];
7169
7481
  const record3 = args;
7170
7482
  const allowed = op === "list" ? /* @__PURE__ */ new Set(["select", "sort", "skip", "limit", "page", "pageSize", "include"]) : /* @__PURE__ */ new Set(["select", "sort", "include"]);
7171
7483
  for (const key of Object.keys(record3)) {
@@ -7187,7 +7499,7 @@ function validateCorrelatedArgsShape(args, op, depth) {
7187
7499
  return [];
7188
7500
  }
7189
7501
  function validateCorrelatedIncludeShape(entry, depth = 0) {
7190
- if (!isPlainObject14(entry)) return ["Correlated include must be an object"];
7502
+ if (!isPlainObject15(entry)) return ["Correlated include must be an object"];
7191
7503
  const record3 = entry;
7192
7504
  if (record3.mode !== "correlated") return ['Correlated include mode must be "correlated"'];
7193
7505
  if (typeof record3.model !== "string" || record3.model.length === 0)
@@ -7200,7 +7512,7 @@ function validateCorrelatedIncludeShape(entry, depth = 0) {
7200
7512
  if ("localField" in record3 || "foreignField" in record3) {
7201
7513
  return ["Correlated includes must not carry localField or foreignField"];
7202
7514
  }
7203
- if (record3.options !== void 0 && !(isPlainObject14(record3.options) && Object.keys(record3.options).length === 0)) {
7515
+ if (record3.options !== void 0 && !(isPlainObject15(record3.options) && Object.keys(record3.options).length === 0)) {
7204
7516
  return ["Correlated include options must be absent or empty"];
7205
7517
  }
7206
7518
  const hasId = record3.id !== void 0;
@@ -7222,7 +7534,7 @@ function validateCorrelatedIncludeShape(entry, depth = 0) {
7222
7534
  return validateCorrelatedArgsShape(record3.args, record3.op, depth);
7223
7535
  }
7224
7536
  function isCorrelatedInclude(value) {
7225
- return isPlainObject14(value) && value.mode === "correlated";
7537
+ return isPlainObject15(value) && value.mode === "correlated";
7226
7538
  }
7227
7539
 
7228
7540
  // src/services/base.ts
@@ -7446,9 +7758,31 @@ function castCountGroups(input, values) {
7446
7758
  // src/services/base.ts
7447
7759
  init_service_authorization();
7448
7760
 
7761
+ // src/meta.ts
7762
+ init_runtime();
7763
+ init_runtime_context();
7764
+ var getRuntime5 = () => getActiveRuntime() ?? defaultRuntime;
7765
+ var getModelRef = (modelName, refPath, runtime = getRuntime5()) => {
7766
+ const direct = runtime.getModelRef(modelName, refPath);
7767
+ if (direct) return direct;
7768
+ if (!refPath.includes(".")) return null;
7769
+ try {
7770
+ const instance = runtime.getModelInstance?.(
7771
+ modelName
7772
+ );
7773
+ const schemaPath = instance?.schema?.path?.(refPath);
7774
+ const element = schemaPath?.$embeddedSchemaType ?? schemaPath?.embeddedSchemaType ?? schemaPath?.caster;
7775
+ const ref = schemaPath?.options?.ref ?? element?.options?.ref ?? null;
7776
+ return typeof ref === "string" && ref.length > 0 ? ref : null;
7777
+ } catch {
7778
+ return null;
7779
+ }
7780
+ };
7781
+
7449
7782
  // src/acl/related-execution.ts
7450
7783
  init_public_admission();
7451
7784
  init_model_output_planning();
7785
+ init_populate_target_configuration();
7452
7786
  init_authorization_snapshot();
7453
7787
 
7454
7788
  // src/acl/service-execution.ts
@@ -7486,9 +7820,9 @@ function createServiceExecution(receiver, access, authorization, applicability =
7486
7820
  receiver,
7487
7821
  binding,
7488
7822
  authorization: call,
7489
- access: readServiceAuthorization(call, binding).access,
7823
+ access: readServiceAuthorizationAccess(call, binding),
7490
7824
  family: execution,
7491
- hydration: resolveHydration(readServiceAuthorization(call, binding).configuration, hydration)
7825
+ hydration: resolveHydration(readServiceAuthorizationHydration(call, binding), hydration)
7492
7826
  })
7493
7827
  );
7494
7828
  return execution;
@@ -7499,7 +7833,7 @@ function resolveHydration(configuration, source) {
7499
7833
  }
7500
7834
  function withExecutionHydration(receiver, parent, source) {
7501
7835
  const record3 = readServiceExecution(receiver, parent);
7502
- const { configuration } = readServiceAuthorization(record3.authorization, record3.binding);
7836
+ const configuration = readServiceAuthorizationHydration(record3.authorization, record3.binding);
7503
7837
  const execution = Object.freeze(/* @__PURE__ */ Object.create(null));
7504
7838
  executions.set(execution, Object.freeze({ ...record3, hydration: resolveHydration(configuration, source) }));
7505
7839
  return execution;
@@ -7536,7 +7870,7 @@ function deriveServiceExecution(receiver, parent, access, applicability = "servi
7536
7870
  const execution = Object.freeze(/* @__PURE__ */ Object.create(null));
7537
7871
  executions.set(
7538
7872
  execution,
7539
- Object.freeze({ ...record3, authorization: call, access: readServiceAuthorization(call, record3.binding).access })
7873
+ Object.freeze({ ...record3, authorization: call, access: readServiceAuthorizationAccess(call, record3.binding) })
7540
7874
  );
7541
7875
  return execution;
7542
7876
  }
@@ -7743,16 +8077,15 @@ function captureRelatedTargets(runtime, modelName, args = {}, depth = 0, globalO
7743
8077
  for (const entry of entries(args.populate)) {
7744
8078
  const path = typeof entry === "string" ? entry : entry.path;
7745
8079
  if (typeof entry !== "string") assertPopulateAccess(entry.access);
7746
- const model = getModelRef(modelName, path);
7747
- if (model && runtime.hasModel(model))
8080
+ const model = getModelRef(modelName, path, runtime);
8081
+ if (model) {
8082
+ const target = capturePopulateTargetConfiguration(runtime, model, `${modelName}.${path}`);
7748
8083
  targets.push({
7749
8084
  path,
7750
8085
  modelName: model,
7751
- configuration: captureModelAuthorizationConfiguration(runtime, model, void 0, globalOptions),
8086
+ configuration: captureModelAuthorizationConfiguration(runtime, model, target.options, globalOptions),
7752
8087
  staticObjectIdReference: staticObjectIdReference(runtime, modelName, path, model)
7753
8088
  });
7754
- else if (model && !(globalOptions.requireRegisteredPopulateModels === void 0 ? true : globalOptions.requireRegisteredPopulateModels)) {
7755
- targets.push({ path, modelName: model, kind: "passthrough", configuration: { catalog: { paths: [] } } });
7756
8089
  }
7757
8090
  }
7758
8091
  for (const entry of entries(args.include)) {
@@ -7938,20 +8271,20 @@ var copyAuthorizedOutput = (value) => {
7938
8271
  transferDocumentAuthorizationGraph(value, copy);
7939
8272
  return copy;
7940
8273
  };
7941
- var isVirtualContainerValueForGuard = (value) => isPlainObject17(value) && "sub" in value;
8274
+ var isVirtualContainerValueForGuard = (value) => isPlainObject18(value) && "sub" in value;
7942
8275
  var getScopeVirtualsForGuard = (rootVirtuals, scopePath) => {
7943
- if (!rootVirtuals || !isPlainObject17(rootVirtuals)) return null;
8276
+ if (!rootVirtuals || !isPlainObject18(rootVirtuals)) return null;
7944
8277
  if (scopePath.length === 0) return rootVirtuals;
7945
8278
  let current = rootVirtuals;
7946
8279
  for (let i = 0; i < scopePath.length; i += 2) {
7947
8280
  const field = scopePath[i];
7948
8281
  const subMarker = scopePath[i + 1];
7949
8282
  if (!field || subMarker !== "sub") return null;
7950
- if (!isPlainObject17(current)) return null;
8283
+ if (!isPlainObject18(current)) return null;
7951
8284
  const container = current[field];
7952
8285
  if (!isVirtualContainerValueForGuard(container)) return null;
7953
8286
  current = container.sub;
7954
- if (!isPlainObject17(current)) return null;
8287
+ if (!isPlainObject18(current)) return null;
7955
8288
  }
7956
8289
  return current;
7957
8290
  };
@@ -8017,7 +8350,7 @@ var sanitizeSubScopeVirtualKeys = (data, snapshot, sub) => {
8017
8350
  for (const entry of value) stripScope(entry, currentScope);
8018
8351
  return;
8019
8352
  }
8020
- if (!isPlainObject17(value)) return;
8353
+ if (!isPlainObject18(value)) return;
8021
8354
  const record3 = value;
8022
8355
  const leaves = getScopeVirtualLeafNames(snapshot, currentScope);
8023
8356
  for (const leaf of leaves) {
@@ -8030,7 +8363,7 @@ var sanitizeSubScopeVirtualKeys = (data, snapshot, sub) => {
8030
8363
  for (const key of Object.keys(record3)) {
8031
8364
  if (leaves.has(key) || containers.has(key)) continue;
8032
8365
  const child = record3[key];
8033
- if (Array.isArray(child) || isPlainObject17(child)) {
8366
+ if (Array.isArray(child) || isPlainObject18(child)) {
8034
8367
  stripScope(child, currentScope);
8035
8368
  }
8036
8369
  }
@@ -8042,7 +8375,7 @@ var LOGICAL_FILTER_KEYS = /* @__PURE__ */ new Set(["$and", "$or", "$nor"]);
8042
8375
  var isOperatorKey = (key) => key.startsWith("$");
8043
8376
  var REMOVED_VIRTUAL_FILTER = /* @__PURE__ */ Symbol("removed virtual filter clause");
8044
8377
  var rewriteFilterObject = (value, visit) => {
8045
- if (!isPlainObject17(value)) return value;
8378
+ if (!isPlainObject18(value)) return value;
8046
8379
  let changed = false;
8047
8380
  const entries = [];
8048
8381
  for (const [key, child] of Object.entries(value)) {
@@ -8073,7 +8406,7 @@ var resolveVirtualFilterField = (path, scope2) => {
8073
8406
  if (!Object.hasOwn(scope2, segment) || scope2[segment] === void 0) return null;
8074
8407
  const definition = scope2[segment];
8075
8408
  if (!isVirtualContainerValueForGuard(definition)) return REMOVED_VIRTUAL_FILTER;
8076
- scope2 = isPlainObject17(definition.sub) ? definition.sub : null;
8409
+ scope2 = isPlainObject18(definition.sub) ? definition.sub : null;
8077
8410
  inContainer = true;
8078
8411
  }
8079
8412
  return scope2;
@@ -8085,7 +8418,7 @@ var stripVirtualFilterOperator = (key, value, scope2) => {
8085
8418
  }
8086
8419
  if (key === "$all") {
8087
8420
  return rewriteFilterClauses(value, (entry) => {
8088
- if (!isPlainObject17(entry) || Object.keys(entry).length !== 1 || !Object.hasOwn(entry, "$elemMatch")) return entry;
8421
+ if (!isPlainObject18(entry) || Object.keys(entry).length !== 1 || !Object.hasOwn(entry, "$elemMatch")) return entry;
8089
8422
  return rewriteFilterObject(entry, (operator, operand) => stripVirtualFilterOperator(operator, operand, scope2));
8090
8423
  });
8091
8424
  }
@@ -8100,7 +8433,7 @@ var stripVirtualFilterQuery = (value, scope2, element = false) => {
8100
8433
  if (isOperatorKey(key)) return element ? stripVirtualFilterOperator(key, child, scope2) : child;
8101
8434
  const fieldScope = resolveVirtualFilterField(key, scope2);
8102
8435
  if (fieldScope === REMOVED_VIRTUAL_FILTER) return REMOVED_VIRTUAL_FILTER;
8103
- if (!fieldScope || !isPlainObject17(child)) return child;
8436
+ if (!fieldScope || !isPlainObject18(child)) return child;
8104
8437
  if (Object.keys(child).some(isOperatorKey)) {
8105
8438
  return rewriteFilterObject(
8106
8439
  child,
@@ -8124,7 +8457,7 @@ function getCorrelatedTemplateDepth(includes) {
8124
8457
  const visit = (entries, depth) => {
8125
8458
  const list = castArray2(entries).filter(Boolean);
8126
8459
  for (const entry of list) {
8127
- if (!isPlainObject17(entry)) continue;
8460
+ if (!isPlainObject18(entry)) continue;
8128
8461
  max = Math.max(max, depth);
8129
8462
  const nested = entry.args?.include;
8130
8463
  if (nested !== void 0) {
@@ -8153,7 +8486,7 @@ function validateClientFilter(filter2) {
8153
8486
  value.forEach((item, index) => visit(item, `${path}[${index}]`));
8154
8487
  return;
8155
8488
  }
8156
- if (!isPlainObject17(value)) return;
8489
+ if (!isPlainObject18(value)) return;
8157
8490
  Object.entries(value).forEach(([key, child]) => {
8158
8491
  const nextPath = path ? `${path}.${key}` : key;
8159
8492
  if (blockedOperators.has(key)) {
@@ -8435,9 +8768,9 @@ var Base = class {
8435
8768
  const walk = (node, prefix) => {
8436
8769
  for (const [key, value] of Object.entries(node)) {
8437
8770
  if (value === void 0) continue;
8438
- if (isPlainObject17(value) && "sub" in value) {
8771
+ if (isPlainObject18(value) && "sub" in value) {
8439
8772
  const sub = value.sub;
8440
- if (isPlainObject17(sub)) walk(sub, prefix ? `${prefix}.${key}` : key);
8773
+ if (isPlainObject18(sub)) walk(sub, prefix ? `${prefix}.${key}` : key);
8441
8774
  continue;
8442
8775
  }
8443
8776
  out.push(prefix ? `${prefix}.${key}` : key);
@@ -8490,7 +8823,7 @@ var Base = class {
8490
8823
  this.throwClientRequestError("bad_request" /* BadRequest */, `Model ${modelName} not found`);
8491
8824
  }
8492
8825
  for (const entry of entries) {
8493
- if (!isPlainObject17(entry)) continue;
8826
+ if (!isPlainObject18(entry)) continue;
8494
8827
  const models = visited.get(entry) ?? /* @__PURE__ */ new Set();
8495
8828
  if (models.has(modelName)) continue;
8496
8829
  models.add(modelName);
@@ -9109,7 +9442,7 @@ import {
9109
9442
  get as get13,
9110
9443
  isArray as isArray10,
9111
9444
  isBoolean as isBoolean4,
9112
- isPlainObject as isPlainObject24,
9445
+ isPlainObject as isPlainObject25,
9113
9446
  omit,
9114
9447
  set as set10,
9115
9448
  uniq as uniq2,
@@ -9210,7 +9543,7 @@ var model_default = Model;
9210
9543
  // src/acl/distinct-field-authorization.ts
9211
9544
  init_field_authorization();
9212
9545
  init_field_catalog();
9213
- import { isPlainObject as isPlainObject18 } from "@web-ts-toolkit/utils";
9546
+ import { isPlainObject as isPlainObject19 } from "@web-ts-toolkit/utils";
9214
9547
  var schemaVariants = (schema) => Object.values(schema.discriminators ?? {});
9215
9548
  var scalarTypes = /* @__PURE__ */ new Set([
9216
9549
  "String",
@@ -9290,8 +9623,8 @@ function isDistinctFieldAuthorized(args) {
9290
9623
  if (path !== field && !path.startsWith(`${field}.`) && !field.startsWith(`${path}.`)) continue;
9291
9624
  if (path.startsWith(`${field}.`) && (!eligible(path) || !schemaShape(schema, path) || !granted(path)))
9292
9625
  return false;
9293
- if (isPlainObject18(raw) && raw.sub !== void 0) {
9294
- if (!isPlainObject18(raw.sub) || !declaredRulesSafe(raw.sub, path, next)) return false;
9626
+ if (isPlainObject19(raw) && raw.sub !== void 0) {
9627
+ if (!isPlainObject19(raw.sub) || !declaredRulesSafe(raw.sub, path, next)) return false;
9295
9628
  }
9296
9629
  }
9297
9630
  return true;
@@ -9469,7 +9802,7 @@ init_populate_target();
9469
9802
 
9470
9803
  // src/helpers/apply-update.ts
9471
9804
  init_lib();
9472
- import { isPlainObject as isPlainObject22, set as set8 } from "@web-ts-toolkit/utils";
9805
+ import { isPlainObject as isPlainObject23, set as set8 } from "@web-ts-toolkit/utils";
9473
9806
  function applyUpdate(target, data, allowedFields) {
9474
9807
  const wholeFields = /* @__PURE__ */ new Set();
9475
9808
  const partialContainers = /* @__PURE__ */ new Set();
@@ -9489,7 +9822,7 @@ function applyUpdate(target, data, allowedFields) {
9489
9822
  const apply = (values, prefix = "") => {
9490
9823
  for (const [key, value] of Object.entries(values)) {
9491
9824
  const path = prefix ? `${prefix}.${key}` : key;
9492
- if (partialContainers.has(path) && !wholeFields.has(path) && (isPlainObject22(value) || Array.isArray(value))) {
9825
+ if (partialContainers.has(path) && !wholeFields.has(path) && (isPlainObject23(value) || Array.isArray(value))) {
9493
9826
  apply(value, path);
9494
9827
  } else if (isDocument2(target)) {
9495
9828
  target.set(path, value);
@@ -9506,7 +9839,7 @@ init_document();
9506
9839
  init_request_work();
9507
9840
  init_logger_helpers();
9508
9841
  init_request_complexity();
9509
- import { get as get10, isPlainObject as isPlainObject23, set as set9 } from "@web-ts-toolkit/utils";
9842
+ import { get as get10, isPlainObject as isPlainObject24, set as set9 } from "@web-ts-toolkit/utils";
9510
9843
  import { mongo as mongo5 } from "mongoose";
9511
9844
 
9512
9845
  // src/acl/select-resolution.ts
@@ -9623,7 +9956,7 @@ init_field_catalog();
9623
9956
  init_service_authorization();
9624
9957
  var own = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
9625
9958
  var inside = (path, root) => path === root || path.startsWith(`${root}.`);
9626
- var record2 = (value) => isPlainObject23(value);
9959
+ var record2 = (value) => isPlainObject24(value);
9627
9960
  function inputValue(value) {
9628
9961
  if (typeof value === "function" || typeof value === "symbol") {
9629
9962
  authorizationError("input values must be persisted data, not functions or symbols");
@@ -10625,7 +10958,8 @@ var Service = class extends Base {
10625
10958
  async finalizeDirectOutput(args) {
10626
10959
  const { plan, context } = args;
10627
10960
  const { authorization, authorizationBinding: binding } = plan;
10628
- const { access, configuration } = readServiceAuthorization(authorization, binding);
10961
+ const access = readServiceAuthorizationAccess(authorization, binding);
10962
+ const configuration = readServiceOutputMetadata(authorization, binding);
10629
10963
  const source = getIncludeSource(args.doc);
10630
10964
  const snapshot = isolateDocForOutput(source);
10631
10965
  for (const key of Object.keys(snapshot))
@@ -10979,10 +11313,6 @@ var Service = class extends Base {
10979
11313
  const target = captured.relatedTargets.find((target2) => target2.path === descriptor.path);
10980
11314
  const modelName = target?.modelName;
10981
11315
  if (!modelName) continue;
10982
- if (target.kind === "passthrough") {
10983
- result2.push({ path: descriptor.path, select: descriptor.select });
10984
- continue;
10985
- }
10986
11316
  const policy = relatedPolicy(state.authorization, state.binding, descriptor.path);
10987
11317
  const svc = this.requestCore.getPublicService(modelName);
10988
11318
  const child = createRelatedExecution(
@@ -11001,7 +11331,7 @@ var Service = class extends Base {
11001
11331
  if (isExecutionRowDenied(svc, child)) continue;
11002
11332
  const childState = readServiceExecution(svc, child);
11003
11333
  const targetConfiguration = readServiceAuthorization(childState.authorization, childState.binding).configuration;
11004
- if (policy?.operation.mode !== "grant" && !await this.hookGate.run(
11334
+ if (targetConfiguration.requireOperationAccessForPopulateQueries !== false && policy?.operation.mode !== "grant" && !await this.hookGate.run(
11005
11335
  () => evaluateRouteGuard(
11006
11336
  this.req,
11007
11337
  getAuthorizationPermissions(childState.authorization, childState.binding),
@@ -11053,7 +11383,7 @@ var Service = class extends Base {
11053
11383
  if (this.isScalarPopulateValue(rawTarget)) return rawTarget;
11054
11384
  const isDoc = (() => {
11055
11385
  try {
11056
- if (isPlainObject24(rawTarget)) return true;
11386
+ if (isPlainObject25(rawTarget)) return true;
11057
11387
  const maybe = rawTarget;
11058
11388
  if (maybe && typeof maybe === "object" && "_doc" in maybe) return true;
11059
11389
  if (maybe && typeof maybe.toObject === "function") return true;
@@ -13456,7 +13786,7 @@ init_helpers();
13456
13786
 
13457
13787
  // src/processors.ts
13458
13788
  init_permissions_alias();
13459
- import { cloneDeep as cloneDeep5, forEach as forEach5, get as get14, hasOwn as hasOwn3, isArray as isArray11, isPlainObject as isPlainObject25, map, set as set11 } from "@web-ts-toolkit/utils";
13789
+ import { cloneDeep as cloneDeep5, forEach as forEach5, get as get14, hasOwn as hasOwn3, isArray as isArray11, isPlainObject as isPlainObject26, map, set as set11 } from "@web-ts-toolkit/utils";
13460
13790
  function cloneProcessorValue(value) {
13461
13791
  const copy = cloneDeep5(value);
13462
13792
  copyPermissionsAliasGraph(value, copy);
@@ -13496,7 +13826,7 @@ function resolveLeafParents(obj, src) {
13496
13826
  for (let ind = 0; ind < segs.length - 1; ind += 1) {
13497
13827
  const seg = segs[ind];
13498
13828
  targets = targets.reduce((ret, target) => {
13499
- if (!isPlainObject25(target)) return ret;
13829
+ if (!isPlainObject26(target)) return ret;
13500
13830
  const targetObject = target;
13501
13831
  const next = targetObject[seg];
13502
13832
  if (isArray11(next)) {
@@ -13510,7 +13840,7 @@ function resolveLeafParents(obj, src) {
13510
13840
  const leaf = segs[segs.length - 1];
13511
13841
  const pairs = [];
13512
13842
  forEach5(targets, (target) => {
13513
- if (!isPlainObject25(target)) return;
13843
+ if (!isPlainObject26(target)) return;
13514
13844
  pairs.push({ parent: target, leaf });
13515
13845
  });
13516
13846
  return pairs;
@@ -13531,7 +13861,7 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
13531
13861
  forEach5(segs, (seg, ind) => {
13532
13862
  if (segs.length === ind + 1) {
13533
13863
  forEach5(targets, (target) => {
13534
- if (!isPlainObject25(target)) return;
13864
+ if (!isPlainObject26(target)) return;
13535
13865
  const targetObject = target;
13536
13866
  const leaf = targetObject[seg];
13537
13867
  const sourceForDest = leaf;
@@ -13539,7 +13869,7 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
13539
13869
  if (isArray11(leaf)) {
13540
13870
  if (leaf.length === 0) {
13541
13871
  depopulated = [];
13542
- } else if (leaf.every((item) => isPlainObject25(item))) {
13872
+ } else if (leaf.every((item) => isPlainObject26(item))) {
13543
13873
  depopulated = map(leaf, idField).map((id, i) => {
13544
13874
  if (id === void 0) {
13545
13875
  failMissingId(`${src}[${i}]`);
@@ -13549,7 +13879,7 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
13549
13879
  } else {
13550
13880
  return;
13551
13881
  }
13552
- } else if (isPlainObject25(leaf)) {
13882
+ } else if (isPlainObject26(leaf)) {
13553
13883
  const id = get14(leaf, idField);
13554
13884
  if (id === void 0) {
13555
13885
  failMissingId(src);
@@ -13563,7 +13893,7 @@ var copyAndDepopulate = (docObject, operations, options = { mutable: true, idFie
13563
13893
  });
13564
13894
  } else {
13565
13895
  targets = targets.reduce((ret, target) => {
13566
- if (!isPlainObject25(target)) return ret;
13896
+ if (!isPlainObject26(target)) return ret;
13567
13897
  const targetObject = target;
13568
13898
  const next = targetObject[seg];
13569
13899
  if (isArray11(next)) {
@@ -13669,7 +13999,7 @@ var countPaths = (docObject, operations, options = { mutable: true }) => {
13669
13999
  let count;
13670
14000
  if (isArray11(leafValue)) {
13671
14001
  count = leafValue.length;
13672
- } else if (isPlainObject25(leafValue)) {
14002
+ } else if (isPlainObject26(leafValue)) {
13673
14003
  count = Object.keys(leafValue).length;
13674
14004
  } else if (leafValue === null || typeof leafValue === "undefined") {
13675
14005
  count = 0;
@@ -13747,6 +14077,8 @@ init_core_shared();
13747
14077
  init_virtual_projection();
13748
14078
  init_populate_target();
13749
14079
  init_model_output_planning();
14080
+ init_authorization_snapshot();
14081
+ init_document();
13750
14082
  init_output_field_filter();
13751
14083
  init_request_work();
13752
14084
  var emptyPermissionLeaf = () => ({ $: "_" });
@@ -13857,17 +14189,11 @@ var Core = class {
13857
14189
  return this.genSelect(modelName, access, targetFields, options);
13858
14190
  }
13859
14191
  /** Persisted request-only populate admission; fetch planning is not field authority. */
13860
- async requestPopulateSelect(modelName, access, targetFields = null, scopePath = []) {
14192
+ async requestPopulateSelect(configuration, permissions, access, targetFields = null, scopePath = []) {
13861
14193
  const subPaths = scopePath.flatMap((part) => [part, "sub"]);
13862
- const permissionSchema = getModelOption(modelName, ["permissionSchema"].concat(subPaths).join("."));
13863
- const permissions = this.getGlobalPermissions();
13864
- let virtualNames;
13865
- try {
13866
- const runtime = getActiveRuntime() ?? defaultRuntime;
13867
- virtualNames = runtime.getVirtualNames(modelName, subPaths);
13868
- } catch {
13869
- virtualNames = [];
13870
- }
14194
+ const permissionSchema = get15(configuration, ["permissionSchema", ...subPaths]);
14195
+ const prefix = scopePath.length ? `${scopePath.join(".")}.` : "";
14196
+ const virtualNames = (configuration.catalog.virtualPaths ?? []).filter((path) => path.startsWith(prefix)).map((path) => path.slice(prefix.length)).filter((path) => !path.includes("."));
13871
14197
  return resolveSelectForRequest({
13872
14198
  req: this.req,
13873
14199
  permissionSchema,
@@ -13886,13 +14212,21 @@ var Core = class {
13886
14212
  assertPublicAdmission(_populate, "populate");
13887
14213
  assertPopulateAccess(access);
13888
14214
  if (!_populate) return [];
13889
- let populate = Array.isArray(_populate) ? _populate : [_populate];
13890
- for (const p of populate) {
14215
+ const descriptors = copyAuthorizationValue(Array.isArray(_populate) ? _populate : [_populate], true);
14216
+ for (const p of descriptors) {
13891
14217
  if (!isString6(p)) assertPopulateAccess(p.access);
13892
14218
  }
13893
- populate = compact3(
14219
+ const runtime = this.requestRuntime;
14220
+ const globalOptions = modelServiceGlobalOptions(this, runtime);
14221
+ const relatedTargets = captureRelatedTargets(runtime, modelName, { populate: descriptors }, 0, globalOptions);
14222
+ const sourceConfiguration = captureModelAuthorizationConfiguration(runtime, modelName, void 0, globalOptions);
14223
+ const originalPermissions = this.getGlobalPermissions();
14224
+ const permissions = new permission_default(
14225
+ Object.fromEntries(originalPermissions.keys.map((key) => [key, originalPermissions.has(key)]))
14226
+ );
14227
+ const populate = compact3(
13894
14228
  await Promise.all(
13895
- populate.map(async (p) => {
14229
+ descriptors.map(async (p) => {
13896
14230
  const populateAccess = !isString6(p) && p.access ? p.access : access;
13897
14231
  assertPopulateAccess(populateAccess);
13898
14232
  const originalSelect = isString6(p) ? void 0 : p.select;
@@ -13904,78 +14238,86 @@ var Core = class {
13904
14238
  const pathForSourcePermission = scopePrefix && ret.path.startsWith(`${scopePrefix}.`) ? ret.path.slice(scopePrefix.length + 1) : ret.path;
13905
14239
  const parentPath = pathForSourcePermission.includes(".") ? pathForSourcePermission.split(".")[0] : pathForSourcePermission;
13906
14240
  const allowedParentPaths = await this.requestPopulateSelect(
13907
- modelName,
14241
+ sourceConfiguration,
14242
+ permissions,
13908
14243
  populateAccess,
13909
14244
  [parentPath],
13910
14245
  scopePath
13911
14246
  );
13912
14247
  if (!allowedParentPaths.includes(parentPath)) return null;
13913
- const refModelName = getModelRef(modelName, ret.path);
13914
- if (!refModelName) return null;
13915
- const runtime = getActiveRuntime();
13916
- if (!runtime) return null;
13917
- const requireRegisteredPopulateModels = getGlobalOption("requireRegisteredPopulateModels", true);
13918
- if (!runtime.hasModel(refModelName)) {
13919
- return requireRegisteredPopulateModels ? null : ret;
13920
- }
13921
- const allowedTargetOperation = await this.isAllowed(refModelName, populateAccess);
13922
- if (!allowedTargetOperation) {
14248
+ const target = relatedTargets.find((entry) => entry.path === ret.path);
14249
+ if (!target) return null;
14250
+ const refModelName = target.modelName;
14251
+ const targetConfiguration = target.configuration;
14252
+ if (targetConfiguration.requireOperationAccessForPopulateQueries !== false && !await withCapturedModelOperationGuard(
14253
+ this,
14254
+ refModelName,
14255
+ populateAccess,
14256
+ getCapturedAuthorizationOption(targetConfiguration, `operationAccess.${populateAccess}`),
14257
+ permissions,
14258
+ () => this.isAllowed(refModelName, populateAccess)
14259
+ )) {
13923
14260
  return null;
13924
14261
  }
13925
- ret.select = await this.requestPopulateSelect(refModelName, populateAccess, ret.select);
13926
- const hookInputs = getModelOption(refModelName, `alwaysSelectFields.${populateAccess}`, []);
13927
- ret.select = uniq3([...ret.select, ...stripVirtuals(hookInputs, runtime.getVirtualNames(refModelName))]);
13928
- const filter2 = await this.genFilter(refModelName, populateAccess, null);
14262
+ ret.select = await this.requestPopulateSelect(
14263
+ targetConfiguration,
14264
+ permissions,
14265
+ populateAccess,
14266
+ ret.select
14267
+ );
14268
+ const hookInputs = resolveAlwaysSelectForAccess(
14269
+ targetConfiguration.alwaysSelectFields,
14270
+ populateAccess
14271
+ );
14272
+ ret.select = uniq3([
14273
+ ...ret.select,
14274
+ ...stripVirtuals(hookInputs, [...targetConfiguration.catalog.virtualPaths ?? []])
14275
+ ]);
14276
+ const match = isString6(p) ? {} : p.match ?? {};
14277
+ const filter2 = await resolveAccessFilterForRequest({
14278
+ req: this.req,
14279
+ permissions,
14280
+ cache: this.caches.baseFilter,
14281
+ cacheKey: `${refModelName}_baseFilter_${populateAccess}`,
14282
+ access: populateAccess,
14283
+ filter: deepIsolateValue(match),
14284
+ getOption: (key) => getCapturedAuthorizationOption(targetConfiguration, key)
14285
+ });
13929
14286
  if (filter2 === false) return null;
13930
- ret.match = filter2;
13931
- try {
13932
- const planAccess = populateAccess;
13933
- const targetSnapshot = {
13934
- permissionSchema: getModelOption(refModelName, "permissionSchema", null) ?? null,
13935
- virtuals: getModelOption(refModelName, "virtuals", null) ?? null,
13936
- alwaysSelectFields: getModelOption(refModelName, "alwaysSelectFields", null) ?? null,
13937
- modelPermissionPrefix: getModelOption(refModelName, "modelPermissionPrefix", "") ?? "",
13938
- requireExplicitSelect: getModelOption(refModelName, "requireExplicitSelect", false) ?? false,
13939
- documentPermissionField: getModelOption(refModelName, "documentPermissionField", "_permissions") ?? "_permissions",
13940
- exposedDocPermissionKeys: getModelOption(refModelName, "exposedDocPermissionKeys", void 0),
13941
- stripPermissionsField: getModelOption(refModelName, "stripPermissionsField", false) ?? false,
13942
- disableFieldPermissions: getModelOption(refModelName, "disableFieldPermissions", false) ?? false
13943
- };
13944
- const perms = this.getPermissions();
13945
- const globalForPlan = perms && typeof perms.has === "function" && typeof perms.hasKey === "function" ? perms : { has: () => false, hasKey: () => false };
13946
- const targetPlan = planVirtualProjection({
13947
- receivingModelName: refModelName,
13948
- snapshot: {
13949
- permissionSchema: targetSnapshot.permissionSchema,
13950
- virtuals: targetSnapshot.virtuals,
13951
- alwaysSelectFields: targetSnapshot.alwaysSelectFields,
13952
- modelPermissionPrefix: targetSnapshot.modelPermissionPrefix,
13953
- requireExplicitSelect: targetSnapshot.requireExplicitSelect,
13954
- documentPermissionField: targetSnapshot.documentPermissionField
13955
- },
13956
- virtualAccess: planAccess,
13957
- outputAccess: planAccess,
13958
- docPermissionsAccess: planAccess,
13959
- requestedSelect: originalSelect,
13960
- internalFetch: { baseFields: ["_id"] },
13961
- globalPermissions: globalForPlan
13962
- });
13963
- const merged = uniq3([...normalizeSelect(ret.select), ...targetPlan.virtualOnlyDeps ?? []]).filter(
13964
- (t) => t !== "-_id" && !t.includes(".sub.")
13965
- );
13966
- if (!merged.includes("_id")) merged.push("_id");
13967
- ret.select = merged;
13968
- setPopulateTargetMeta(ret, {
13969
- targetModelName: refModelName,
13970
- virtualAccess: planAccess,
13971
- outputAccess: planAccess,
13972
- docPermissionsAccess: planAccess,
13973
- requestedSelect: originalSelect,
13974
- plan: targetPlan,
13975
- snapshot: targetSnapshot
13976
- });
13977
- } catch {
13978
- }
14287
+ ret.match = deepIsolateValue(intersectFilters(match, filter2));
14288
+ const planAccess = populateAccess;
14289
+ const targetSnapshot = targetConfiguration;
14290
+ const targetPlan = planVirtualProjection({
14291
+ receivingModelName: refModelName,
14292
+ snapshot: {
14293
+ permissionSchema: targetSnapshot.permissionSchema,
14294
+ virtuals: targetSnapshot.virtuals,
14295
+ alwaysSelectFields: targetSnapshot.alwaysSelectFields,
14296
+ modelPermissionPrefix: targetSnapshot.modelPermissionPrefix,
14297
+ requireExplicitSelect: targetSnapshot.requireExplicitSelect,
14298
+ documentPermissionField: targetSnapshot.documentPermissionField
14299
+ },
14300
+ virtualAccess: planAccess,
14301
+ outputAccess: planAccess,
14302
+ docPermissionsAccess: planAccess,
14303
+ requestedSelect: originalSelect,
14304
+ internalFetch: { baseFields: ["_id"] },
14305
+ globalPermissions: permissions
14306
+ });
14307
+ const merged = uniq3([...normalizeSelect(ret.select), ...targetPlan.virtualOnlyDeps ?? []]).filter(
14308
+ (t) => t !== "-_id" && !t.includes(".sub.")
14309
+ );
14310
+ if (!merged.includes("_id")) merged.push("_id");
14311
+ ret.select = merged;
14312
+ setPopulateTargetMeta(ret, {
14313
+ targetModelName: refModelName,
14314
+ virtualAccess: planAccess,
14315
+ outputAccess: planAccess,
14316
+ docPermissionsAccess: planAccess,
14317
+ requestedSelect: originalSelect,
14318
+ plan: targetPlan,
14319
+ snapshot: targetSnapshot
14320
+ });
13979
14321
  return ret;
13980
14322
  })
13981
14323
  )
@@ -14299,12 +14641,12 @@ function guard(condition) {
14299
14641
  const permissions = req[PERMISSIONS];
14300
14642
  let cond = condition;
14301
14643
  let phas = (key) => permissions.has(key);
14302
- if (isPlainObject26(condition)) {
14644
+ if (isPlainObject27(condition)) {
14303
14645
  const { modelName, id, condition: _cond } = condition;
14304
14646
  const svc = req.macl.getPublicService(modelName);
14305
14647
  const select = getModelOption(modelName, `alwaysSelectFields.read`, void 0);
14306
14648
  let _id = isString7(id) ? id : null;
14307
- if (isPlainObject26(id)) {
14649
+ if (isPlainObject27(id)) {
14308
14650
  const { type = "param", key } = id;
14309
14651
  if (type === "param") {
14310
14652
  const paramValue = req.params[key];
@@ -14346,7 +14688,7 @@ import express2 from "express";
14346
14688
 
14347
14689
  // src/routers/model-router.ts
14348
14690
  import JsonRouter8 from "@web-ts-toolkit/express-json-router";
14349
- import { forEach as forEach7, isPlainObject as isPlainObject28, isString as isString8, isUndefined as isUndefined2, normalizeUrlPath as normalizeUrlPath3, padEnd } from "@web-ts-toolkit/utils";
14691
+ import { forEach as forEach7, isPlainObject as isPlainObject29, isString as isString8, isUndefined as isUndefined2, normalizeUrlPath as normalizeUrlPath3, padEnd } from "@web-ts-toolkit/utils";
14350
14692
  init_logger_helpers();
14351
14693
  init_runtime();
14352
14694
  init_runtime_context();
@@ -14480,7 +14822,7 @@ import JsonRouter7 from "@web-ts-toolkit/express-json-router";
14480
14822
 
14481
14823
  // src/validation/common.ts
14482
14824
  import { z as z2 } from "zod";
14483
- import { isPlainObject as isPlainObject27 } from "@web-ts-toolkit/utils";
14825
+ import { isPlainObject as isPlainObject28 } from "@web-ts-toolkit/utils";
14484
14826
 
14485
14827
  // src/validation/public-admission-schema.ts
14486
14828
  init_public_admission();
@@ -14609,7 +14951,7 @@ var correlatedFilterSchema = z2.record(z2.string(), z2.unknown()).superRefine((f
14609
14951
  }
14610
14952
  });
14611
14953
  var correlatedEmptyOptionsSchema = z2.unknown().refine(
14612
- (value) => value === void 0 || isPlainObject27(value) && Object.keys(value).length === 0,
14954
+ (value) => value === void 0 || isPlainObject28(value) && Object.keys(value).length === 0,
14613
14955
  "Correlated include options must be absent or empty"
14614
14956
  ).optional();
14615
14957
  var correlatedPathSchema = z2.string().min(1).superRefine((path, ctx) => {
@@ -14646,7 +14988,7 @@ var correlatedListArgsSchema = z2.object({
14646
14988
  include: z2.lazy(() => correlatedNestedIncludeSchema).optional()
14647
14989
  }).strict().superRefine((args, ctx) => checkArgsMarkers(args.select, args.sort, ctx));
14648
14990
  var correlatedCountArgsSchema = z2.unknown().refine(
14649
- (value) => value === void 0 || isPlainObject27(value) && Object.keys(value).length === 0,
14991
+ (value) => value === void 0 || isPlainObject28(value) && Object.keys(value).length === 0,
14650
14992
  "Count includes accept no args"
14651
14993
  ).optional();
14652
14994
  var correlatedReadByIdSchema = z2.object({
@@ -14699,7 +15041,7 @@ var correlatedNestedIncludeSchema = z2.union([includeItemSchema, z2.array(includ
14699
15041
  var rejectDuplicateCorrelatedPaths = (items, ctx) => {
14700
15042
  const seen = /* @__PURE__ */ new Set();
14701
15043
  items.forEach((item, index) => {
14702
- if (isPlainObject27(item) && item.mode === "correlated" && typeof item.path === "string") {
15044
+ if (isPlainObject28(item) && item.mode === "correlated" && typeof item.path === "string") {
14703
15045
  if (seen.has(item.path)) {
14704
15046
  ctx.addIssue({
14705
15047
  code: z2.ZodIssueCode.custom,
@@ -16700,7 +17042,7 @@ var ModelRouter = class _ModelRouter {
16700
17042
  value
16701
17043
  );
16702
17044
  }
16703
- if (arguments.length === 1 && isPlainObject28(keyOrOptions)) {
17045
+ if (arguments.length === 1 && isPlainObject29(keyOrOptions)) {
16704
17046
  assertMutableRouterOptions("model", keyOrOptions);
16705
17047
  this.runtime.setModelOptions(
16706
17048
  this.modelName,
@@ -17234,7 +17576,7 @@ var RootRouter = class {
17234
17576
 
17235
17577
  // src/routers/data-router.ts
17236
17578
  import JsonRouter10 from "@web-ts-toolkit/express-json-router";
17237
- import { isPlainObject as isPlainObject29, isString as isString9, isUndefined as isUndefined3, normalizeUrlPath as normalizeUrlPath5 } from "@web-ts-toolkit/utils";
17579
+ import { isPlainObject as isPlainObject30, isString as isString9, isUndefined as isUndefined3, normalizeUrlPath as normalizeUrlPath5 } from "@web-ts-toolkit/utils";
17238
17580
  init_runtime();
17239
17581
  init_helpers();
17240
17582
  init_schemas();
@@ -17450,7 +17792,7 @@ var DataRouter = class {
17450
17792
  value
17451
17793
  );
17452
17794
  }
17453
- if (arguments.length === 1 && isPlainObject29(keyOrOptions)) {
17795
+ if (arguments.length === 1 && isPlainObject30(keyOrOptions)) {
17454
17796
  assertMutableRouterOptions("data", keyOrOptions);
17455
17797
  this.runtime.setDataOptions(this.dataName, keyOrOptions);
17456
17798
  }
@@ -17609,7 +17951,7 @@ function createRuntimeApi(runtime) {
17609
17951
  if (arguments.length === 2 && isString10(keyOrOptions)) {
17610
17952
  return runtime.setGlobalOption(keyOrOptions, value);
17611
17953
  }
17612
- if (arguments.length === 1 && isPlainObject30(keyOrOptions)) {
17954
+ if (arguments.length === 1 && isPlainObject31(keyOrOptions)) {
17613
17955
  return runtime.setGlobalOptions(keyOrOptions);
17614
17956
  }
17615
17957
  };