@web-ts-toolkit/access-router 0.47.0 → 0.48.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/llms.txt CHANGED
@@ -171,7 +171,8 @@ Business boundary contracts (see the corresponding README sections):
171
171
  - `Service.exists`: terminal explicit/base/override false now returns a Forbidden ErrorResult before adapter dispatch in both includeId modes (previously successful false/null). Allowed matches/misses remain true/false, or `{ _id }`/null with includeId. Check success before data.
172
172
  - Public `_read`/`_readFilter` tryList never retries Forbidden/BadRequest. Authorized misses retain list fallback and its operation guard. `_upsert` with _id propagates denied exists as Forbidden (previously Unauthorized); allowed miss remains Unauthorized, with no create in either case. Generated direct/root-entry denial status is 403 (root envelope 200). Custom Express routes must map ErrorResult codes to HTTP status themselves; res.json does not do this automatically.
173
173
  - Shared-request cross-runtime composition keeps independent state: runtime B never reuses runtime A's permissions or base-filter cache, even with the same `requestPermissionField` name. Same-runtime middleware repeats reuse state without rerunning `globalPermissions`. Pre-populated request permission fields are application-supplied and preserved.
174
- - Advanced mutation routes persist the final parsed body: nested `data` validators run before the whole-body (`default`) validator, and only final `data`/`select`/`populate`/`tasks` plus allowed `options` (`includePermissions`/`populateAccess`, plus `returningAll` for update/upsert) reach the service. Missing body options fall back to `returning_all`/`include_permissions` query params.
174
+ - Advanced mutation routes persist the final parsed body: nested `data` validators run before the whole-body (`default`) validator, and only final `data`/`select`/`populate`/`tasks` plus allowed `options` (`includePermissions`/`includeFieldPermissions`/`populateAccess`, plus `returningAll` for update/upsert) reach the service. Missing body options fall back to `returning_all`/`include_permissions` query params. Simple GET routes also accept `include_field_permissions` query params.
175
+ - Router permission-output postures (absolute over client input and per-operation `defaults`): `stripPermissionsField: true` removes the whole permissions field from outputs (output matches `includePermissions: false` with the field, including its placeholder, deleted; enforcement still runs); `disableFieldPermissions: true` never computes `_view`/`_edit` maps while `includePermissions` still controls `docPermissions`-hook keys.
175
176
  - `package.json` declares `sideEffects` array listing the bundled runtime entries that run the lazy `mongoose-schema-jsonschema` patch on first runtime construction — bundlers must retain those entries
176
177
 
177
178
  ## Pointers
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@web-ts-toolkit/access-router",
3
3
  "description": "ACL-aware Express routers and in-memory data services for Mongoose-backed APIs",
4
4
  "homepage": "https://web-ts-toolkit.pages.dev/docs/packages/access-router",
5
- "version": "0.47.0",
5
+ "version": "0.48.0",
6
6
  "sideEffects": [
7
7
  "./**/index.js",
8
8
  "./**/index.mjs",
@@ -57,8 +57,8 @@
57
57
  "node": ">=22"
58
58
  },
59
59
  "dependencies": {
60
- "@web-ts-toolkit/express-json-router": "0.47.0",
61
- "@web-ts-toolkit/utils": "0.47.0",
60
+ "@web-ts-toolkit/express-json-router": "0.48.0",
61
+ "@web-ts-toolkit/utils": "0.48.0",
62
62
  "just-diff": "^6.0.2",
63
63
  "mongoose-schema-jsonschema": "^4.0.1",
64
64
  "sift": "^17.1.3",
@@ -39,8 +39,28 @@ declare class Service<TModel = unknown> extends Base<TModel> {
39
39
  }): any;
40
40
  protected createModelAdapter(modelName: string): any;
41
41
  protected getModelRouterOptions(modelName: string): ModelRouterOptions<TModel>;
42
+ /**
43
+ * Router-level permission output postures (see `ModelRouterOptions`).
44
+ * Absolute: they win over client input and per-operation `defaults`.
45
+ */
46
+ getPermissionsPosture(): {
47
+ stripPermissionsField: boolean;
48
+ disableFieldPermissions: boolean;
49
+ };
50
+ /**
51
+ * Router posture `stripPermissionsField`: drop the whole document
52
+ * permissions field (including the empty placeholder) from output instead
53
+ * of emitting permission metadata. Returns true when applied.
54
+ */
55
+ private stripPermissionsOutput;
42
56
  private asServiceHookContext;
43
57
  private getEffectiveAllowedSortFields;
58
+ /**
59
+ * With `requireExplicitSelect`, an omitted client select behaves as an
60
+ * `_id`-only projection, yielding field-less rows. Explicit selects pass
61
+ * through untouched.
62
+ */
63
+ protected resolveEffectiveSelect(select: Projection | undefined): Projection | undefined;
44
64
  private beginOp;
45
65
  private completeOp;
46
66
  constructor(req: ModelRequest, modelName: string);
@@ -54,6 +74,7 @@ declare class Service<TModel = unknown> extends Base<TModel> {
54
74
  }, options?: {
55
75
  skim?: boolean;
56
76
  includePermissions?: boolean;
77
+ includeFieldPermissions?: boolean;
57
78
  }): Promise<SingleResult<TModel>>;
58
79
  /**
59
80
  * Update at authorized policy paths: admitted leaves preserve omitted siblings;
@@ -157,6 +178,7 @@ declare class PublicService<TModel = unknown> extends Service<TModel> {
157
178
  }, options?: {
158
179
  skim?: boolean;
159
180
  includePermissions?: boolean;
181
+ includeFieldPermissions?: boolean;
160
182
  }): Promise<SingleResult<PublicOutput<TModel>>>;
161
183
  _read<TSelect extends Projection | undefined = undefined, TPopulate extends Populate[] | string | undefined = undefined>(id: string, args?: Omit<PublicReadArgs, 'select' | 'populate'> & {
162
184
  select?: TSelect;
@@ -398,22 +420,28 @@ type ListQueryInput = {
398
420
  page_size?: string;
399
421
  skim?: 'true' | 'false';
400
422
  include_permissions?: 'true' | 'false';
423
+ include_field_permissions?: 'true' | 'false';
401
424
  include_count?: 'true' | 'false';
402
425
  include_extra_headers?: 'true' | 'false';
403
426
  };
404
427
  type CreateQueryInput = {
405
428
  include_permissions?: 'true' | 'false';
429
+ include_field_permissions?: 'true' | 'false';
406
430
  };
407
431
  type ReadQueryInput = {
408
432
  include_permissions?: 'true' | 'false';
433
+ include_field_permissions?: 'true' | 'false';
409
434
  try_list?: 'true' | 'false';
410
435
  };
411
436
  type UpdateQueryInput = {
412
437
  returning_all?: 'true' | 'false';
438
+ include_permissions?: 'true' | 'false';
439
+ include_field_permissions?: 'true' | 'false';
413
440
  };
414
441
  type UpsertQueryInput = {
415
442
  returning_all?: 'true' | 'false';
416
443
  include_permissions?: 'true' | 'false';
444
+ include_field_permissions?: 'true' | 'false';
417
445
  };
418
446
  type AdvancedListBody = {
419
447
  filter?: Filter | unknown[];
@@ -429,6 +457,7 @@ type AdvancedListBody = {
429
457
  options?: {
430
458
  skim?: boolean;
431
459
  includePermissions?: boolean;
460
+ includeFieldPermissions?: boolean;
432
461
  includeCount?: boolean;
433
462
  includeExtraHeaders?: boolean;
434
463
  populateAccess?: unknown;
@@ -447,6 +476,7 @@ type AdvancedReadFilterBody = {
447
476
  options?: {
448
477
  skim?: boolean;
449
478
  includePermissions?: boolean;
479
+ includeFieldPermissions?: boolean;
450
480
  tryList?: boolean;
451
481
  populateAccess?: unknown;
452
482
  };
@@ -459,6 +489,7 @@ type AdvancedReadBody = {
459
489
  options?: {
460
490
  skim?: boolean;
461
491
  includePermissions?: boolean;
492
+ includeFieldPermissions?: boolean;
462
493
  tryList?: boolean;
463
494
  populateAccess?: unknown;
464
495
  };
@@ -470,6 +501,7 @@ type AdvancedCreateBody = {
470
501
  tasks?: Task | Task[];
471
502
  options?: {
472
503
  includePermissions?: boolean;
504
+ includeFieldPermissions?: boolean;
473
505
  populateAccess?: unknown;
474
506
  };
475
507
  };
@@ -481,6 +513,7 @@ type AdvancedUpdateBody = {
481
513
  options?: {
482
514
  returningAll?: boolean;
483
515
  includePermissions?: boolean;
516
+ includeFieldPermissions?: boolean;
484
517
  populateAccess?: unknown;
485
518
  };
486
519
  };
@@ -492,6 +525,7 @@ type AdvancedUpsertBody = {
492
525
  options?: {
493
526
  returningAll?: boolean;
494
527
  includePermissions?: boolean;
528
+ includeFieldPermissions?: boolean;
495
529
  populateAccess?: unknown;
496
530
  };
497
531
  };
@@ -952,6 +986,15 @@ interface CreateArgs extends Omit<PublicCreateArgs, 'select' | 'tasks'> {
952
986
  interface PublicCreateOptions {
953
987
  skim?: boolean;
954
988
  includePermissions?: boolean;
989
+ /**
990
+ * Include field-level `_view`/`_edit` maps. Defaults to `includePermissions`
991
+ * when omitted. `includePermissions: true` + `includeFieldPermissions: false`
992
+ * keeps `docPermissions`-hook keys but omits `_view`/`_edit` entirely.
993
+ * Enabling field maps also computes doc permissions as grant input (even
994
+ * under skim or when `includePermissions` is false), so the maps match the
995
+ * `includePermissions: true` output.
996
+ */
997
+ includeFieldPermissions?: boolean;
955
998
  populateAccess?: PopulateAccess;
956
999
  }
957
1000
  interface CreateOptions extends PublicCreateOptions {
@@ -971,6 +1014,15 @@ interface PublicListArgs {
971
1014
  interface PublicListOptions {
972
1015
  skim?: boolean;
973
1016
  includePermissions?: boolean;
1017
+ /**
1018
+ * Include field-level `_view`/`_edit` maps. Defaults to `includePermissions`
1019
+ * when omitted. `includePermissions: true` + `includeFieldPermissions: false`
1020
+ * keeps `docPermissions`-hook keys but omits `_view`/`_edit` entirely.
1021
+ * Enabling field maps also computes doc permissions as grant input (even
1022
+ * under skim or when `includePermissions` is false), so the maps match the
1023
+ * `includePermissions: true` output.
1024
+ */
1025
+ includeFieldPermissions?: boolean;
974
1026
  includeCount?: boolean;
975
1027
  populateAccess?: PopulateAccess;
976
1028
  lean?: boolean;
@@ -987,6 +1039,15 @@ interface PublicReadOptions {
987
1039
  populateAccess?: PopulateAccess;
988
1040
  lean?: boolean;
989
1041
  includePermissions?: boolean;
1042
+ /**
1043
+ * Include field-level `_view`/`_edit` maps. Defaults to `includePermissions`
1044
+ * when omitted. `includePermissions: true` + `includeFieldPermissions: false`
1045
+ * keeps `docPermissions`-hook keys but omits `_view`/`_edit` entirely.
1046
+ * Enabling field maps also computes doc permissions as grant input (even
1047
+ * under skim or when `includePermissions` is false), so the maps match the
1048
+ * `includePermissions: true` output.
1049
+ */
1050
+ includeFieldPermissions?: boolean;
990
1051
  /** Retry an authorized read miss with list access (default true); requires list operation access. Forbidden/BadRequest never retry. */
991
1052
  tryList?: boolean;
992
1053
  }
@@ -1018,6 +1079,15 @@ interface PublicUpdateOptions {
1018
1079
  skim?: boolean;
1019
1080
  returningAll?: boolean;
1020
1081
  includePermissions?: boolean;
1082
+ /**
1083
+ * Include field-level `_view`/`_edit` maps. Defaults to `includePermissions`
1084
+ * when omitted. `includePermissions: true` + `includeFieldPermissions: false`
1085
+ * keeps `docPermissions`-hook keys but omits `_view`/`_edit` entirely.
1086
+ * Enabling field maps also computes doc permissions as grant input (even
1087
+ * under skim or when `includePermissions` is false), so the maps match the
1088
+ * `includePermissions: true` output.
1089
+ */
1090
+ includeFieldPermissions?: boolean;
1021
1091
  populateAccess?: PopulateAccess;
1022
1092
  }
1023
1093
  interface PublicUpsertOptions extends PublicUpdateOptions {
@@ -1302,6 +1372,7 @@ interface RootModelNewQueryEntry extends RootQueryEntryBase<'model', 'new'> {
1302
1372
  options?: {
1303
1373
  skim?: boolean;
1304
1374
  includePermissions?: boolean;
1375
+ includeFieldPermissions?: boolean;
1305
1376
  };
1306
1377
  }
1307
1378
  interface RootModelListQueryEntry extends RootQueryEntryBase<'model', 'list'> {
@@ -1431,7 +1502,7 @@ interface Task {
1431
1502
  options?: Record<string, unknown>;
1432
1503
  }
1433
1504
  interface AccessRouterBaseRequest extends AccessRouterRequest {
1434
- query: Record<'skip' | 'limit' | 'page' | 'page_size' | 'try_list' | 'skim' | 'include_permissions' | 'include_count' | 'include_extra_headers' | 'returning_all', string>;
1505
+ query: Record<'skip' | 'limit' | 'page' | 'page_size' | 'try_list' | 'skim' | 'include_permissions' | 'include_field_permissions' | 'include_count' | 'include_extra_headers' | 'returning_all', string>;
1435
1506
  }
1436
1507
  interface ModelRequest extends AccessRouterBaseRequest {
1437
1508
  macl: Core;
@@ -1522,6 +1593,15 @@ interface FindArgs<T = unknown> {
1522
1593
  interface FindOptions {
1523
1594
  skim?: boolean;
1524
1595
  includePermissions?: boolean;
1596
+ /**
1597
+ * Include field-level `_view`/`_edit` maps. Defaults to `includePermissions`
1598
+ * when omitted. `includePermissions: true` + `includeFieldPermissions: false`
1599
+ * keeps `docPermissions`-hook keys but omits `_view`/`_edit` entirely.
1600
+ * Enabling field maps also computes doc permissions as grant input (even
1601
+ * under skim or when `includePermissions` is false), so the maps match the
1602
+ * `includePermissions: true` output.
1603
+ */
1604
+ includeFieldPermissions?: boolean;
1525
1605
  includeCount?: boolean;
1526
1606
  populateAccess?: PopulateAccess;
1527
1607
  lean?: boolean;
@@ -1544,6 +1624,15 @@ interface FindOneOptions {
1544
1624
  skim?: boolean;
1545
1625
  lean?: boolean;
1546
1626
  includePermissions?: boolean;
1627
+ /**
1628
+ * Include field-level `_view`/`_edit` maps. Defaults to `includePermissions`
1629
+ * when omitted. `includePermissions: true` + `includeFieldPermissions: false`
1630
+ * keeps `docPermissions`-hook keys but omits `_view`/`_edit` entirely.
1631
+ * Enabling field maps also computes doc permissions as grant input (even
1632
+ * under skim or when `includePermissions` is false), so the maps match the
1633
+ * `includePermissions: true` output.
1634
+ */
1635
+ includeFieldPermissions?: boolean;
1547
1636
  }
1548
1637
  interface FindByIdArgs<T = unknown> {
1549
1638
  select?: Projection;
@@ -1716,6 +1805,18 @@ interface DefaultModelRouterOptions<TModel = unknown> {
1716
1805
  mutationRouteSegment?: string;
1717
1806
  operationAccess?: Validation | OperationAccess;
1718
1807
  modelPermissionPrefix?: string;
1808
+ /**
1809
+ * When `true`, list/read/update requests without an explicit `select` return
1810
+ * field-less rows (only `_id`, plus the permission field when requested)
1811
+ * instead of all allowed fields. Applies to plain and advanced PATCH
1812
+ * (returned updated record honors `select`, including `?select=` on plain
1813
+ * PATCH and body `select` on advanced PATCH). Only an omitted (`undefined`)
1814
+ * select triggers this; explicit selects — including backend `defaults` and
1815
+ * an explicitly empty string/array (which keep their current meaning) — are
1816
+ * unaffected. Counts, ordering, includes and permission enforcement are
1817
+ * unchanged. Defaults to `false`.
1818
+ */
1819
+ requireExplicitSelect?: boolean;
1719
1820
  /**
1720
1821
  * Extra fields allowed in `sort` regardless of `list`/`read` field policy.
1721
1822
  * Merged as a union with permission-derived allowed fields (`id`/`_id` are
@@ -1729,6 +1830,25 @@ interface DefaultModelRouterOptions<TModel = unknown> {
1729
1830
  * Defaults to `false` (strict: disallowed sort returns `BadRequest`).
1730
1831
  */
1731
1832
  stripDisallowedSort?: boolean;
1833
+ /**
1834
+ * When `true`, the document permissions field (`documentPermissionField`,
1835
+ * default `_permissions`) is removed from all outputs — including its empty
1836
+ * placeholder — and client `includePermissions`/`includeFieldPermissions`
1837
+ * input (query params, body/root-batch options and service-direct options)
1838
+ * as well as per-operation `defaults` are ignored (both resolve to `false`).
1839
+ * Output matches `includePermissions: false` with the permissions field
1840
+ * removed entirely. Enforcement still runs; only output metadata is
1841
+ * affected. Defaults to `false`.
1842
+ */
1843
+ stripPermissionsField?: boolean;
1844
+ /**
1845
+ * When `true`, client `includeFieldPermissions` input (query params,
1846
+ * body/root-batch options and service-direct options) as well as
1847
+ * per-operation `defaults` are ignored and field maps (`_view`/`_edit`)
1848
+ * are never computed. `docPermissions`-hook keys still follow
1849
+ * `includePermissions`. Defaults to `false`.
1850
+ */
1851
+ disableFieldPermissions?: boolean;
1732
1852
  }
1733
1853
  interface ExtendedDefaultModelRouterOptions<TModel = unknown> extends DefaultModelRouterOptions<TModel> {
1734
1854
  'operationAccess.default'?: Validation;
@@ -1754,6 +1874,16 @@ interface ModelRouterOptions<TModel = unknown> extends DefaultModelRouterOptions
1754
1874
  permissionSchema?: PermissionSchema<AccessRouterFieldKey<TModel>>;
1755
1875
  alwaysSelectFields?: string[];
1756
1876
  docPermissions?: DocPermissions | ModelDocPermissionsHook;
1877
+ /**
1878
+ * Allowlist of `docPermissions`-hook keys exposed in API responses.
1879
+ * Enforcement always uses the full hook map; only serialization is reduced,
1880
+ * after all grant computations and decorate hooks have run. `_view`/`_edit`
1881
+ * are always kept. `undefined` (default) exposes everything (current
1882
+ * behavior); any array (including `[]`) exposes `_view`/`_edit` plus the
1883
+ * listed keys. Note `guard()` conditions on stripped doc-level keys fail
1884
+ * closed — allowlist any key a custom route guard evaluates.
1885
+ */
1886
+ exposedDocPermissionKeys?: string[];
1757
1887
  baseFilter?: ModelBaseFilterHook | Record<string, ModelBaseFilterHook>;
1758
1888
  overrideFilter?: ModelOverrideFilterHook | Record<string, ModelOverrideFilterHook>;
1759
1889
  decorate?: ModelHook<TModel> | Record<string, ModelHook<TModel>>;
@@ -39,8 +39,28 @@ declare class Service<TModel = unknown> extends Base<TModel> {
39
39
  }): any;
40
40
  protected createModelAdapter(modelName: string): any;
41
41
  protected getModelRouterOptions(modelName: string): ModelRouterOptions<TModel>;
42
+ /**
43
+ * Router-level permission output postures (see `ModelRouterOptions`).
44
+ * Absolute: they win over client input and per-operation `defaults`.
45
+ */
46
+ getPermissionsPosture(): {
47
+ stripPermissionsField: boolean;
48
+ disableFieldPermissions: boolean;
49
+ };
50
+ /**
51
+ * Router posture `stripPermissionsField`: drop the whole document
52
+ * permissions field (including the empty placeholder) from output instead
53
+ * of emitting permission metadata. Returns true when applied.
54
+ */
55
+ private stripPermissionsOutput;
42
56
  private asServiceHookContext;
43
57
  private getEffectiveAllowedSortFields;
58
+ /**
59
+ * With `requireExplicitSelect`, an omitted client select behaves as an
60
+ * `_id`-only projection, yielding field-less rows. Explicit selects pass
61
+ * through untouched.
62
+ */
63
+ protected resolveEffectiveSelect(select: Projection | undefined): Projection | undefined;
44
64
  private beginOp;
45
65
  private completeOp;
46
66
  constructor(req: ModelRequest, modelName: string);
@@ -54,6 +74,7 @@ declare class Service<TModel = unknown> extends Base<TModel> {
54
74
  }, options?: {
55
75
  skim?: boolean;
56
76
  includePermissions?: boolean;
77
+ includeFieldPermissions?: boolean;
57
78
  }): Promise<SingleResult<TModel>>;
58
79
  /**
59
80
  * Update at authorized policy paths: admitted leaves preserve omitted siblings;
@@ -157,6 +178,7 @@ declare class PublicService<TModel = unknown> extends Service<TModel> {
157
178
  }, options?: {
158
179
  skim?: boolean;
159
180
  includePermissions?: boolean;
181
+ includeFieldPermissions?: boolean;
160
182
  }): Promise<SingleResult<PublicOutput<TModel>>>;
161
183
  _read<TSelect extends Projection | undefined = undefined, TPopulate extends Populate[] | string | undefined = undefined>(id: string, args?: Omit<PublicReadArgs, 'select' | 'populate'> & {
162
184
  select?: TSelect;
@@ -398,22 +420,28 @@ type ListQueryInput = {
398
420
  page_size?: string;
399
421
  skim?: 'true' | 'false';
400
422
  include_permissions?: 'true' | 'false';
423
+ include_field_permissions?: 'true' | 'false';
401
424
  include_count?: 'true' | 'false';
402
425
  include_extra_headers?: 'true' | 'false';
403
426
  };
404
427
  type CreateQueryInput = {
405
428
  include_permissions?: 'true' | 'false';
429
+ include_field_permissions?: 'true' | 'false';
406
430
  };
407
431
  type ReadQueryInput = {
408
432
  include_permissions?: 'true' | 'false';
433
+ include_field_permissions?: 'true' | 'false';
409
434
  try_list?: 'true' | 'false';
410
435
  };
411
436
  type UpdateQueryInput = {
412
437
  returning_all?: 'true' | 'false';
438
+ include_permissions?: 'true' | 'false';
439
+ include_field_permissions?: 'true' | 'false';
413
440
  };
414
441
  type UpsertQueryInput = {
415
442
  returning_all?: 'true' | 'false';
416
443
  include_permissions?: 'true' | 'false';
444
+ include_field_permissions?: 'true' | 'false';
417
445
  };
418
446
  type AdvancedListBody = {
419
447
  filter?: Filter | unknown[];
@@ -429,6 +457,7 @@ type AdvancedListBody = {
429
457
  options?: {
430
458
  skim?: boolean;
431
459
  includePermissions?: boolean;
460
+ includeFieldPermissions?: boolean;
432
461
  includeCount?: boolean;
433
462
  includeExtraHeaders?: boolean;
434
463
  populateAccess?: unknown;
@@ -447,6 +476,7 @@ type AdvancedReadFilterBody = {
447
476
  options?: {
448
477
  skim?: boolean;
449
478
  includePermissions?: boolean;
479
+ includeFieldPermissions?: boolean;
450
480
  tryList?: boolean;
451
481
  populateAccess?: unknown;
452
482
  };
@@ -459,6 +489,7 @@ type AdvancedReadBody = {
459
489
  options?: {
460
490
  skim?: boolean;
461
491
  includePermissions?: boolean;
492
+ includeFieldPermissions?: boolean;
462
493
  tryList?: boolean;
463
494
  populateAccess?: unknown;
464
495
  };
@@ -470,6 +501,7 @@ type AdvancedCreateBody = {
470
501
  tasks?: Task | Task[];
471
502
  options?: {
472
503
  includePermissions?: boolean;
504
+ includeFieldPermissions?: boolean;
473
505
  populateAccess?: unknown;
474
506
  };
475
507
  };
@@ -481,6 +513,7 @@ type AdvancedUpdateBody = {
481
513
  options?: {
482
514
  returningAll?: boolean;
483
515
  includePermissions?: boolean;
516
+ includeFieldPermissions?: boolean;
484
517
  populateAccess?: unknown;
485
518
  };
486
519
  };
@@ -492,6 +525,7 @@ type AdvancedUpsertBody = {
492
525
  options?: {
493
526
  returningAll?: boolean;
494
527
  includePermissions?: boolean;
528
+ includeFieldPermissions?: boolean;
495
529
  populateAccess?: unknown;
496
530
  };
497
531
  };
@@ -952,6 +986,15 @@ interface CreateArgs extends Omit<PublicCreateArgs, 'select' | 'tasks'> {
952
986
  interface PublicCreateOptions {
953
987
  skim?: boolean;
954
988
  includePermissions?: boolean;
989
+ /**
990
+ * Include field-level `_view`/`_edit` maps. Defaults to `includePermissions`
991
+ * when omitted. `includePermissions: true` + `includeFieldPermissions: false`
992
+ * keeps `docPermissions`-hook keys but omits `_view`/`_edit` entirely.
993
+ * Enabling field maps also computes doc permissions as grant input (even
994
+ * under skim or when `includePermissions` is false), so the maps match the
995
+ * `includePermissions: true` output.
996
+ */
997
+ includeFieldPermissions?: boolean;
955
998
  populateAccess?: PopulateAccess;
956
999
  }
957
1000
  interface CreateOptions extends PublicCreateOptions {
@@ -971,6 +1014,15 @@ interface PublicListArgs {
971
1014
  interface PublicListOptions {
972
1015
  skim?: boolean;
973
1016
  includePermissions?: boolean;
1017
+ /**
1018
+ * Include field-level `_view`/`_edit` maps. Defaults to `includePermissions`
1019
+ * when omitted. `includePermissions: true` + `includeFieldPermissions: false`
1020
+ * keeps `docPermissions`-hook keys but omits `_view`/`_edit` entirely.
1021
+ * Enabling field maps also computes doc permissions as grant input (even
1022
+ * under skim or when `includePermissions` is false), so the maps match the
1023
+ * `includePermissions: true` output.
1024
+ */
1025
+ includeFieldPermissions?: boolean;
974
1026
  includeCount?: boolean;
975
1027
  populateAccess?: PopulateAccess;
976
1028
  lean?: boolean;
@@ -987,6 +1039,15 @@ interface PublicReadOptions {
987
1039
  populateAccess?: PopulateAccess;
988
1040
  lean?: boolean;
989
1041
  includePermissions?: boolean;
1042
+ /**
1043
+ * Include field-level `_view`/`_edit` maps. Defaults to `includePermissions`
1044
+ * when omitted. `includePermissions: true` + `includeFieldPermissions: false`
1045
+ * keeps `docPermissions`-hook keys but omits `_view`/`_edit` entirely.
1046
+ * Enabling field maps also computes doc permissions as grant input (even
1047
+ * under skim or when `includePermissions` is false), so the maps match the
1048
+ * `includePermissions: true` output.
1049
+ */
1050
+ includeFieldPermissions?: boolean;
990
1051
  /** Retry an authorized read miss with list access (default true); requires list operation access. Forbidden/BadRequest never retry. */
991
1052
  tryList?: boolean;
992
1053
  }
@@ -1018,6 +1079,15 @@ interface PublicUpdateOptions {
1018
1079
  skim?: boolean;
1019
1080
  returningAll?: boolean;
1020
1081
  includePermissions?: boolean;
1082
+ /**
1083
+ * Include field-level `_view`/`_edit` maps. Defaults to `includePermissions`
1084
+ * when omitted. `includePermissions: true` + `includeFieldPermissions: false`
1085
+ * keeps `docPermissions`-hook keys but omits `_view`/`_edit` entirely.
1086
+ * Enabling field maps also computes doc permissions as grant input (even
1087
+ * under skim or when `includePermissions` is false), so the maps match the
1088
+ * `includePermissions: true` output.
1089
+ */
1090
+ includeFieldPermissions?: boolean;
1021
1091
  populateAccess?: PopulateAccess;
1022
1092
  }
1023
1093
  interface PublicUpsertOptions extends PublicUpdateOptions {
@@ -1302,6 +1372,7 @@ interface RootModelNewQueryEntry extends RootQueryEntryBase<'model', 'new'> {
1302
1372
  options?: {
1303
1373
  skim?: boolean;
1304
1374
  includePermissions?: boolean;
1375
+ includeFieldPermissions?: boolean;
1305
1376
  };
1306
1377
  }
1307
1378
  interface RootModelListQueryEntry extends RootQueryEntryBase<'model', 'list'> {
@@ -1431,7 +1502,7 @@ interface Task {
1431
1502
  options?: Record<string, unknown>;
1432
1503
  }
1433
1504
  interface AccessRouterBaseRequest extends AccessRouterRequest {
1434
- query: Record<'skip' | 'limit' | 'page' | 'page_size' | 'try_list' | 'skim' | 'include_permissions' | 'include_count' | 'include_extra_headers' | 'returning_all', string>;
1505
+ query: Record<'skip' | 'limit' | 'page' | 'page_size' | 'try_list' | 'skim' | 'include_permissions' | 'include_field_permissions' | 'include_count' | 'include_extra_headers' | 'returning_all', string>;
1435
1506
  }
1436
1507
  interface ModelRequest extends AccessRouterBaseRequest {
1437
1508
  macl: Core;
@@ -1522,6 +1593,15 @@ interface FindArgs<T = unknown> {
1522
1593
  interface FindOptions {
1523
1594
  skim?: boolean;
1524
1595
  includePermissions?: boolean;
1596
+ /**
1597
+ * Include field-level `_view`/`_edit` maps. Defaults to `includePermissions`
1598
+ * when omitted. `includePermissions: true` + `includeFieldPermissions: false`
1599
+ * keeps `docPermissions`-hook keys but omits `_view`/`_edit` entirely.
1600
+ * Enabling field maps also computes doc permissions as grant input (even
1601
+ * under skim or when `includePermissions` is false), so the maps match the
1602
+ * `includePermissions: true` output.
1603
+ */
1604
+ includeFieldPermissions?: boolean;
1525
1605
  includeCount?: boolean;
1526
1606
  populateAccess?: PopulateAccess;
1527
1607
  lean?: boolean;
@@ -1544,6 +1624,15 @@ interface FindOneOptions {
1544
1624
  skim?: boolean;
1545
1625
  lean?: boolean;
1546
1626
  includePermissions?: boolean;
1627
+ /**
1628
+ * Include field-level `_view`/`_edit` maps. Defaults to `includePermissions`
1629
+ * when omitted. `includePermissions: true` + `includeFieldPermissions: false`
1630
+ * keeps `docPermissions`-hook keys but omits `_view`/`_edit` entirely.
1631
+ * Enabling field maps also computes doc permissions as grant input (even
1632
+ * under skim or when `includePermissions` is false), so the maps match the
1633
+ * `includePermissions: true` output.
1634
+ */
1635
+ includeFieldPermissions?: boolean;
1547
1636
  }
1548
1637
  interface FindByIdArgs<T = unknown> {
1549
1638
  select?: Projection;
@@ -1716,6 +1805,18 @@ interface DefaultModelRouterOptions<TModel = unknown> {
1716
1805
  mutationRouteSegment?: string;
1717
1806
  operationAccess?: Validation | OperationAccess;
1718
1807
  modelPermissionPrefix?: string;
1808
+ /**
1809
+ * When `true`, list/read/update requests without an explicit `select` return
1810
+ * field-less rows (only `_id`, plus the permission field when requested)
1811
+ * instead of all allowed fields. Applies to plain and advanced PATCH
1812
+ * (returned updated record honors `select`, including `?select=` on plain
1813
+ * PATCH and body `select` on advanced PATCH). Only an omitted (`undefined`)
1814
+ * select triggers this; explicit selects — including backend `defaults` and
1815
+ * an explicitly empty string/array (which keep their current meaning) — are
1816
+ * unaffected. Counts, ordering, includes and permission enforcement are
1817
+ * unchanged. Defaults to `false`.
1818
+ */
1819
+ requireExplicitSelect?: boolean;
1719
1820
  /**
1720
1821
  * Extra fields allowed in `sort` regardless of `list`/`read` field policy.
1721
1822
  * Merged as a union with permission-derived allowed fields (`id`/`_id` are
@@ -1729,6 +1830,25 @@ interface DefaultModelRouterOptions<TModel = unknown> {
1729
1830
  * Defaults to `false` (strict: disallowed sort returns `BadRequest`).
1730
1831
  */
1731
1832
  stripDisallowedSort?: boolean;
1833
+ /**
1834
+ * When `true`, the document permissions field (`documentPermissionField`,
1835
+ * default `_permissions`) is removed from all outputs — including its empty
1836
+ * placeholder — and client `includePermissions`/`includeFieldPermissions`
1837
+ * input (query params, body/root-batch options and service-direct options)
1838
+ * as well as per-operation `defaults` are ignored (both resolve to `false`).
1839
+ * Output matches `includePermissions: false` with the permissions field
1840
+ * removed entirely. Enforcement still runs; only output metadata is
1841
+ * affected. Defaults to `false`.
1842
+ */
1843
+ stripPermissionsField?: boolean;
1844
+ /**
1845
+ * When `true`, client `includeFieldPermissions` input (query params,
1846
+ * body/root-batch options and service-direct options) as well as
1847
+ * per-operation `defaults` are ignored and field maps (`_view`/`_edit`)
1848
+ * are never computed. `docPermissions`-hook keys still follow
1849
+ * `includePermissions`. Defaults to `false`.
1850
+ */
1851
+ disableFieldPermissions?: boolean;
1732
1852
  }
1733
1853
  interface ExtendedDefaultModelRouterOptions<TModel = unknown> extends DefaultModelRouterOptions<TModel> {
1734
1854
  'operationAccess.default'?: Validation;
@@ -1754,6 +1874,16 @@ interface ModelRouterOptions<TModel = unknown> extends DefaultModelRouterOptions
1754
1874
  permissionSchema?: PermissionSchema<AccessRouterFieldKey<TModel>>;
1755
1875
  alwaysSelectFields?: string[];
1756
1876
  docPermissions?: DocPermissions | ModelDocPermissionsHook;
1877
+ /**
1878
+ * Allowlist of `docPermissions`-hook keys exposed in API responses.
1879
+ * Enforcement always uses the full hook map; only serialization is reduced,
1880
+ * after all grant computations and decorate hooks have run. `_view`/`_edit`
1881
+ * are always kept. `undefined` (default) exposes everything (current
1882
+ * behavior); any array (including `[]`) exposes `_view`/`_edit` plus the
1883
+ * listed keys. Note `guard()` conditions on stripped doc-level keys fail
1884
+ * closed — allowlist any key a custom route guard evaluates.
1885
+ */
1886
+ exposedDocPermissionKeys?: string[];
1757
1887
  baseFilter?: ModelBaseFilterHook | Record<string, ModelBaseFilterHook>;
1758
1888
  overrideFilter?: ModelOverrideFilterHook | Record<string, ModelOverrideFilterHook>;
1759
1889
  decorate?: ModelHook<TModel> | Record<string, ModelHook<TModel>>;