@objectstack/plugin-approvals 17.1.0 → 17.2.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/CHANGELOG.md +397 -0
- package/dist/index.d.mts +365 -3
- package/dist/index.d.ts +365 -3
- package/dist/index.js +525 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +525 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -12
package/dist/index.d.ts
CHANGED
|
@@ -364,11 +364,18 @@ declare const SysApprovalRequest: Omit<{
|
|
|
364
364
|
maxAge: string;
|
|
365
365
|
onlyWhen?: Record<string, string | number | boolean | {
|
|
366
366
|
$in: (string | number)[];
|
|
367
|
+
} | {
|
|
368
|
+
$null: boolean;
|
|
367
369
|
}> | undefined;
|
|
368
370
|
} | undefined;
|
|
369
371
|
ttl?: {
|
|
370
372
|
field: string;
|
|
371
373
|
expireAfter: string;
|
|
374
|
+
onlyWhen?: Record<string, string | number | boolean | {
|
|
375
|
+
$in: (string | number)[];
|
|
376
|
+
} | {
|
|
377
|
+
$null: boolean;
|
|
378
|
+
}> | undefined;
|
|
372
379
|
} | undefined;
|
|
373
380
|
storage?: {
|
|
374
381
|
strategy: "rotation";
|
|
@@ -4566,11 +4573,18 @@ declare const SysApprovalAction: Omit<{
|
|
|
4566
4573
|
maxAge: string;
|
|
4567
4574
|
onlyWhen?: Record<string, string | number | boolean | {
|
|
4568
4575
|
$in: (string | number)[];
|
|
4576
|
+
} | {
|
|
4577
|
+
$null: boolean;
|
|
4569
4578
|
}> | undefined;
|
|
4570
4579
|
} | undefined;
|
|
4571
4580
|
ttl?: {
|
|
4572
4581
|
field: string;
|
|
4573
4582
|
expireAfter: string;
|
|
4583
|
+
onlyWhen?: Record<string, string | number | boolean | {
|
|
4584
|
+
$in: (string | number)[];
|
|
4585
|
+
} | {
|
|
4586
|
+
$null: boolean;
|
|
4587
|
+
}> | undefined;
|
|
4574
4588
|
} | undefined;
|
|
4575
4589
|
storage?: {
|
|
4576
4590
|
strategy: "rotation";
|
|
@@ -7129,11 +7143,18 @@ declare const SysApprovalApprover: Omit<{
|
|
|
7129
7143
|
maxAge: string;
|
|
7130
7144
|
onlyWhen?: Record<string, string | number | boolean | {
|
|
7131
7145
|
$in: (string | number)[];
|
|
7146
|
+
} | {
|
|
7147
|
+
$null: boolean;
|
|
7132
7148
|
}> | undefined;
|
|
7133
7149
|
} | undefined;
|
|
7134
7150
|
ttl?: {
|
|
7135
7151
|
field: string;
|
|
7136
7152
|
expireAfter: string;
|
|
7153
|
+
onlyWhen?: Record<string, string | number | boolean | {
|
|
7154
|
+
$in: (string | number)[];
|
|
7155
|
+
} | {
|
|
7156
|
+
$null: boolean;
|
|
7157
|
+
}> | undefined;
|
|
7137
7158
|
} | undefined;
|
|
7138
7159
|
storage?: {
|
|
7139
7160
|
strategy: "rotation";
|
|
@@ -8671,11 +8692,18 @@ declare const SysApprovalDelegation: Omit<{
|
|
|
8671
8692
|
maxAge: string;
|
|
8672
8693
|
onlyWhen?: Record<string, string | number | boolean | {
|
|
8673
8694
|
$in: (string | number)[];
|
|
8695
|
+
} | {
|
|
8696
|
+
$null: boolean;
|
|
8674
8697
|
}> | undefined;
|
|
8675
8698
|
} | undefined;
|
|
8676
8699
|
ttl?: {
|
|
8677
8700
|
field: string;
|
|
8678
8701
|
expireAfter: string;
|
|
8702
|
+
onlyWhen?: Record<string, string | number | boolean | {
|
|
8703
|
+
$in: (string | number)[];
|
|
8704
|
+
} | {
|
|
8705
|
+
$null: boolean;
|
|
8706
|
+
}> | undefined;
|
|
8679
8707
|
} | undefined;
|
|
8680
8708
|
storage?: {
|
|
8681
8709
|
strategy: "rotation";
|
|
@@ -10443,6 +10471,64 @@ declare const SysApprovalDelegation: Omit<{
|
|
|
10443
10471
|
};
|
|
10444
10472
|
}, "fields">;
|
|
10445
10473
|
|
|
10474
|
+
/**
|
|
10475
|
+
* Read-time redaction of the approval payload snapshot (#10749).
|
|
10476
|
+
*
|
|
10477
|
+
* ## The defect this closes
|
|
10478
|
+
*
|
|
10479
|
+
* `sys_approval_request.payload_json` stores the submitted record's raw row,
|
|
10480
|
+
* captured from the flow's `$record` variable — which the automation layer
|
|
10481
|
+
* hands over with **the record's own FLS never applying**. The column is a
|
|
10482
|
+
* `textarea` on `sys_approval_request`, so to every read door it is an opaque
|
|
10483
|
+
* string: the field-visibility machinery governs *columns of objects* and
|
|
10484
|
+
* cannot see inside a JSON column. Every field-level read control an app
|
|
10485
|
+
* author declared on the SUBJECT object — `requiredPermissions` (ADR-0066 D3),
|
|
10486
|
+
* a permission set marking a field non-readable, a `maskingRule` — is
|
|
10487
|
+
* therefore unenforceable on the approval path, for every app.
|
|
10488
|
+
*
|
|
10489
|
+
* ## The shape of the fix (maintainer ruling 2026-08-22, Option B)
|
|
10490
|
+
*
|
|
10491
|
+
* The full snapshot **stays at rest**: the approval record remains audit
|
|
10492
|
+
* evidence of what was actually submitted. Redaction is applied at SERVE time,
|
|
10493
|
+
* keyed on the reading caller, so the same row answers an admin with the whole
|
|
10494
|
+
* snapshot and a restricted approver with only the fields they may read.
|
|
10495
|
+
*
|
|
10496
|
+
* The readable set is not recomputed here. It comes from the security
|
|
10497
|
+
* service's `getReadableFields(object, context)` — documented as the same
|
|
10498
|
+
* field mask the read middleware applies, so this seam cannot drift from
|
|
10499
|
+
* data-plane FLS, and the CSV/XLSX export path already derives its columns
|
|
10500
|
+
* from it.
|
|
10501
|
+
*
|
|
10502
|
+
* ## What this deliberately does NOT do
|
|
10503
|
+
*
|
|
10504
|
+
* - **It does not gate on OBJECT-level access.** An approver routinely has no
|
|
10505
|
+
* read grant on the object under approval at all — that is the normal shape
|
|
10506
|
+
* of an approval, and the snapshot is how they see what they are approving.
|
|
10507
|
+
* `getReadableFields` answers with the full field set for a caller who has
|
|
10508
|
+
* no field-permission entries for the object, which is exactly the behaviour
|
|
10509
|
+
* this seam wants: FIELD-level declarations are enforced, object-level
|
|
10510
|
+
* access is untouched, and every approval drawer shipping today keeps
|
|
10511
|
+
* rendering.
|
|
10512
|
+
* - **It does not act on `hidden: true`.** `hidden` is a UI contract ("Hidden
|
|
10513
|
+
* from default UI") and, in the spec's own words, "has never governed
|
|
10514
|
+
* serialization" — measurably: no read path in the repo strips a value on
|
|
10515
|
+
* it. Making it govern serialization here alone would make the approval path
|
|
10516
|
+
* stricter than a direct read of the very same row (so it would close no
|
|
10517
|
+
* leak — the approver can just read the record), while breaking every drawer
|
|
10518
|
+
* that renders a `hidden` business column. That is a `packages/spec`
|
|
10519
|
+
* semantics question, and it is left open rather than decided here.
|
|
10520
|
+
*/
|
|
10521
|
+
/** The slice of the security service this seam consumes. */
|
|
10522
|
+
interface FieldVisibilitySource {
|
|
10523
|
+
/**
|
|
10524
|
+
* Fields of `object` this context may read — the same mask the ObjectQL read
|
|
10525
|
+
* middleware applies. `undefined` when the object schema cannot be resolved;
|
|
10526
|
+
* `[]` is the security plugin's own fail-closed tier (unresolvable posture,
|
|
10527
|
+
* dangling on-behalf-of delegator).
|
|
10528
|
+
*/
|
|
10529
|
+
getReadableFields(object: string, context?: unknown): Promise<string[] | undefined>;
|
|
10530
|
+
}
|
|
10531
|
+
|
|
10446
10532
|
/**
|
|
10447
10533
|
* Node-era approval runtime (ADR-0019).
|
|
10448
10534
|
*
|
|
@@ -10608,7 +10694,7 @@ interface ApprovalServiceOptions {
|
|
|
10608
10694
|
clock?: ApprovalClock;
|
|
10609
10695
|
logger?: {
|
|
10610
10696
|
info?: (msg: any, ...rest: any[]) => void;
|
|
10611
|
-
warn
|
|
10697
|
+
warn: (msg: any, ...rest: any[]) => void;
|
|
10612
10698
|
error?: (msg: any, ...rest: any[]) => void;
|
|
10613
10699
|
debug?: (msg: any, ...rest: any[]) => void;
|
|
10614
10700
|
};
|
|
@@ -10635,6 +10721,14 @@ interface ApprovalServiceOptions {
|
|
|
10635
10721
|
* guard stands down.
|
|
10636
10722
|
*/
|
|
10637
10723
|
tenancyPosture?: () => string | undefined;
|
|
10724
|
+
/**
|
|
10725
|
+
* [#10749] Field-visibility authority used to redact the payload snapshot at
|
|
10726
|
+
* serve time. Usually attached after construction via
|
|
10727
|
+
* {@link ApprovalService.attachFieldVisibility}, because the security plugin
|
|
10728
|
+
* may register after this one. Absent ⇒ snapshots are served unredacted,
|
|
10729
|
+
* exactly as before this seam landed.
|
|
10730
|
+
*/
|
|
10731
|
+
fieldVisibility?: FieldVisibilitySource;
|
|
10638
10732
|
/**
|
|
10639
10733
|
* [#8652] Objects on which a user holding READ access to the target business
|
|
10640
10734
|
* record may also see that record's approval requests and action history —
|
|
@@ -10652,6 +10746,7 @@ declare class ApprovalService implements IApprovalService {
|
|
|
10652
10746
|
private messaging?;
|
|
10653
10747
|
private publicBaseUrl;
|
|
10654
10748
|
private tenancyPosture?;
|
|
10749
|
+
private fieldVisibility?;
|
|
10655
10750
|
/**
|
|
10656
10751
|
* [#8652] The enabled object set for the record-reader visibility tier.
|
|
10657
10752
|
* EMPTY means the tier is off — the default, and the shape every existing
|
|
@@ -10661,6 +10756,29 @@ declare class ApprovalService implements IApprovalService {
|
|
|
10661
10756
|
constructor(opts: ApprovalServiceOptions);
|
|
10662
10757
|
/** Attach (or replace) the ADR-0105 D9 posture provider. */
|
|
10663
10758
|
attachTenancyPosture(provider: () => string | undefined): void;
|
|
10759
|
+
/**
|
|
10760
|
+
* [#10749] Attach (or replace) the field-visibility authority the payload
|
|
10761
|
+
* redaction seam reads. Late-bound: plugin load order does not guarantee the
|
|
10762
|
+
* security service exists when this one is constructed.
|
|
10763
|
+
*/
|
|
10764
|
+
attachFieldVisibility(source: FieldVisibilitySource | undefined): void;
|
|
10765
|
+
/**
|
|
10766
|
+
* [#10749] Redact each row's payload snapshot down to the fields the READING
|
|
10767
|
+
* caller may see on that row's subject object.
|
|
10768
|
+
*
|
|
10769
|
+
* Runs BEFORE {@link ApprovalService.enrichRows}, and that ordering is
|
|
10770
|
+
* load-bearing rather than incidental: `enrichRows` derives `payload_display`
|
|
10771
|
+
* (lookup foreign keys inside the snapshot resolved to referenced record
|
|
10772
|
+
* titles) and `payload_labels` (a label per snapshot key) by WALKING THE
|
|
10773
|
+
* SNAPSHOT'S OWN KEYS. Redact first and both derived maps are clean for free;
|
|
10774
|
+
* redact after and a restricted field's name, its authored label and the
|
|
10775
|
+
* title of the record it points at all still ship — the value would be gone
|
|
10776
|
+
* and the disclosure would not.
|
|
10777
|
+
*
|
|
10778
|
+
* Rows are grouped by subject object so one `getReadableFields` call covers a
|
|
10779
|
+
* whole page of same-object requests.
|
|
10780
|
+
*/
|
|
10781
|
+
private redactPayloads;
|
|
10664
10782
|
/** Deps bundle for the ADR-0105 D9 org-scope helpers. */
|
|
10665
10783
|
private get orgScopeDeps();
|
|
10666
10784
|
/**
|
|
@@ -10789,8 +10907,110 @@ declare class ApprovalService implements IApprovalService {
|
|
|
10789
10907
|
* expression's own values, pre-expansion) for the `__resolvedFrom` audit.
|
|
10790
10908
|
*/
|
|
10791
10909
|
private resolveExpressionApprovers;
|
|
10792
|
-
/**
|
|
10910
|
+
/**
|
|
10911
|
+
* Flat team — `sys_team` is better-auth's collaboration grouping (no hierarchy).
|
|
10912
|
+
*
|
|
10913
|
+
* Takes an organization for the reason every sibling expansion does
|
|
10914
|
+
* ({@link expandBusinessUnitUsers}, {@link expandPositionUsers},
|
|
10915
|
+
* {@link expandMembershipTierUsers}): an approver expansion answers "who, in
|
|
10916
|
+
* THIS organization". Before #10230 this one did not ask, and it was the last
|
|
10917
|
+
* expansion that did not — a `team` approver naming ANOTHER organization's
|
|
10918
|
+
* team routed that organization's people an approval over a record they are
|
|
10919
|
+
* not a tenant of.
|
|
10920
|
+
*
|
|
10921
|
+
* TWO screens run here, and they assert different things (#10230, #10547):
|
|
10922
|
+
*
|
|
10923
|
+
* 1. the TEAM must not provably belong to another organization
|
|
10924
|
+
* ({@link teamIsProvablyOutsideOrg}) — `sys_team` carries
|
|
10925
|
+
* `organization_id` outright
|
|
10926
|
+
* (`packages/platform-objects/src/identity/sys-team.object.ts`), so a
|
|
10927
|
+
* team id transitively names exactly one organization and ONE row
|
|
10928
|
+
* answers the question;
|
|
10929
|
+
* 2. each expanded MEMBER must not provably hold membership only in other
|
|
10930
|
+
* organizations ({@link dropMembersProvablyOutsideOrg}) —
|
|
10931
|
+
* `sys_team_member` carries `team_id` and `user_id` and NO tenancy
|
|
10932
|
+
* column at all, so passing (1) says nothing whatever about the people
|
|
10933
|
+
* it lists.
|
|
10934
|
+
*
|
|
10935
|
+
* #10230 landed (1) alone and deferred (2) on purpose. What closed the
|
|
10936
|
+
* deferral is that (1) does not imply (2) even a little: a member removed
|
|
10937
|
+
* from the organization but left on the team, a team re-parented across
|
|
10938
|
+
* organizations (`/organization/update-team` accepts `organizationId` in its
|
|
10939
|
+
* partial body), or a `sys_team_member` row written by a seed rather than
|
|
10940
|
+
* through better-auth all produce a team that passes (1) carrying a user who
|
|
10941
|
+
* is provably a tenant of somewhere else. Measured on this tree, not read off
|
|
10942
|
+
* the schema — the probe is quoted in `team-member-org-screen.test.ts`.
|
|
10943
|
+
*
|
|
10944
|
+
* (2) is the SAME assertion as {@link managerIsProvablyOutsideOrg}, one hop
|
|
10945
|
+
* further out, and it is asserted the same way: `sys_user` carries no tenancy
|
|
10946
|
+
* fact, so `sys_member` rows are the only evidence that a person is placed
|
|
10947
|
+
* anywhere. Like that screen, this one grants no reads and applies no read
|
|
10948
|
+
* screen to any approver type that lacks one today, so it decides nothing
|
|
10949
|
+
* #7497 (does approver routing imply record read visibility?) asks.
|
|
10950
|
+
*/
|
|
10793
10951
|
private expandTeamUsers;
|
|
10952
|
+
/**
|
|
10953
|
+
* Is `teamId` PROVABLY a team of a DIFFERENT organization? (#10230)
|
|
10954
|
+
*
|
|
10955
|
+
* "Provably" carries the same posture the sibling screen states at length in
|
|
10956
|
+
* {@link managerIsProvablyOutsideOrg}, for the same reasons:
|
|
10957
|
+
*
|
|
10958
|
+
* - the team row carries an `organization_id` and it is not the request's
|
|
10959
|
+
* ⇒ the tenancy fact is present and NEGATIVE ⇒ screen it out;
|
|
10960
|
+
* - the row carries no `organization_id`, does not exist, or the read failed
|
|
10961
|
+
* ⇒ the tenancy fact is ABSENT ⇒ leave routing exactly as it was.
|
|
10962
|
+
*
|
|
10963
|
+
* The `organization_id = null` limb is not timidity — it is the reading
|
|
10964
|
+
* {@link businessUnitOrgScope} settled on one screen below, for the identical
|
|
10965
|
+
* shape: null on a platform object means "owned by no organization", which is
|
|
10966
|
+
* what a seed writes because a seed cannot know the organization id the
|
|
10967
|
+
* runtime mints at boot. Treating null as "not mine" would delete every
|
|
10968
|
+
* seeded team approver at once — a larger behaviour change than the hole
|
|
10969
|
+
* being closed. Measured, and not hypothetically: this package's own
|
|
10970
|
+
* `team_ok` expansion fixture is exactly such a stack (it has
|
|
10971
|
+
* `sys_team_member` rows, a request carrying an organization, and no
|
|
10972
|
+
* `sys_team` row at all).
|
|
10973
|
+
*
|
|
10974
|
+
* Screening the TEAM before reading its members is also what keeps the cost
|
|
10975
|
+
* at one row: a team that fails the screen never fans out.
|
|
10976
|
+
*/
|
|
10977
|
+
private teamIsProvablyOutsideOrg;
|
|
10978
|
+
/**
|
|
10979
|
+
* Drop the expanded team members who are PROVABLY tenants of other
|
|
10980
|
+
* organizations and not of `organizationId`. (#10547)
|
|
10981
|
+
*
|
|
10982
|
+
* Returns the survivors, in the order they were expanded.
|
|
10983
|
+
*
|
|
10984
|
+
* Posture — identical to {@link managerIsProvablyOutsideOrg} and
|
|
10985
|
+
* {@link teamIsProvablyOutsideOrg}, deliberately, because it is the same
|
|
10986
|
+
* assertion about the same table:
|
|
10987
|
+
*
|
|
10988
|
+
* - membership rows exist for this user, none in `organizationId`
|
|
10989
|
+
* ⇒ the tenancy fact is present and NEGATIVE ⇒ drop him;
|
|
10990
|
+
* - no membership rows at all for him, the read failed, or the request
|
|
10991
|
+
* carries no organization
|
|
10992
|
+
* ⇒ the tenancy fact is ABSENT ⇒ leave routing exactly as it was.
|
|
10993
|
+
*
|
|
10994
|
+
* The absent limb is load-bearing rather than timid, and #3807 is the recorded
|
|
10995
|
+
* cost of getting it wrong: a stack that stamps an organization on requests
|
|
10996
|
+
* but never materializes `sys_member` rows would otherwise lose EVERY team
|
|
10997
|
+
* approver at once. This package's own `team_ok` expansion fixture and
|
|
10998
|
+
* #10230's T2/T3 fixtures are exactly such stacks — they carry team rows and
|
|
10999
|
+
* a request organization and no `sys_member` table at all — so the absent
|
|
11000
|
+
* limb is exercised by neighbours on every run of this suite.
|
|
11001
|
+
*
|
|
11002
|
+
* ONE read for the whole slate, never one per person: the expansion is capped
|
|
11003
|
+
* at 10000 members and a per-user query would turn a single team approver
|
|
11004
|
+
* into 10000 round trips.
|
|
11005
|
+
*
|
|
11006
|
+
* ⚠️ A TRUNCATED read fails open, and that is the subtle half. This read is
|
|
11007
|
+
* the only evidence that a member IS a tenant here, so a result cut off at
|
|
11008
|
+
* the limit could be missing the very row that keeps a legitimate approver on
|
|
11009
|
+
* the slate — screening him out on missing evidence, which inverts the
|
|
11010
|
+
* posture into fail-CLOSED precisely where it must not. When the read comes
|
|
11011
|
+
* back at the cap it is treated as no evidence at all.
|
|
11012
|
+
*/
|
|
11013
|
+
private dropMembersProvablyOutsideOrg;
|
|
10794
11014
|
/**
|
|
10795
11015
|
* Tenant scope for a `sys_business_unit` read that may legitimately be
|
|
10796
11016
|
* env-wide (#3807).
|
|
@@ -10876,7 +11096,64 @@ declare class ApprovalService implements IApprovalService {
|
|
|
10876
11096
|
* change here changes position routing too.
|
|
10877
11097
|
*/
|
|
10878
11098
|
private expandMembershipTierUsers;
|
|
11099
|
+
/**
|
|
11100
|
+
* `sys_user.manager_id`, screened to the request's organization (#10153).
|
|
11101
|
+
*
|
|
11102
|
+
* Takes an organization argument for the same reason its siblings do
|
|
11103
|
+
* ({@link expandPositionUsers}, {@link expandMembershipTierUsers}): an
|
|
11104
|
+
* approver expansion answers "who, in THIS organization". Before #10153 this
|
|
11105
|
+
* one did not ask, and it was the only expansion that did not — a
|
|
11106
|
+
* `manager_id` pointing at a person in another organization routed that
|
|
11107
|
+
* person an approval over a record they are not a tenant of.
|
|
11108
|
+
*
|
|
11109
|
+
* ⚠️ The screen reads `sys_member`, which LOOKS like the D2 read-visibility
|
|
11110
|
+
* filter next to it ({@link filterApproversWhoCanRead}). It is not, and this
|
|
11111
|
+
* comment exists so the next reader does not conclude that #7497 (does
|
|
11112
|
+
* approver routing imply record read visibility?) was settled here. It was
|
|
11113
|
+
* not. Two facts make this the SIBLING treatment rather than a
|
|
11114
|
+
* read-visibility ruling:
|
|
11115
|
+
*
|
|
11116
|
+
* 1. Two of the three org-scoped expansions already screen on exactly this
|
|
11117
|
+
* column — `expandMembershipTierUsers` filters `sys_member.organization_id`
|
|
11118
|
+
* outright, and it is also the second limb of `expandPositionUsers`. So
|
|
11119
|
+
* `sys_member.organization_id` is already this file's answer to "which
|
|
11120
|
+
* organization is this person in", independent of what they may read.
|
|
11121
|
+
* 2. `sys_user` carries no `organization_id` at all. It is a GLOBAL identity
|
|
11122
|
+
* table, so a membership row is the only tenancy fact that exists for a
|
|
11123
|
+
* user — there is no other read this screen could have been written with.
|
|
11124
|
+
*
|
|
11125
|
+
* This change grants no reads and applies no read screen to any type that
|
|
11126
|
+
* lacks one today, so it decides nothing #7497 asks.
|
|
11127
|
+
*/
|
|
10879
11128
|
private lookupManager;
|
|
11129
|
+
/**
|
|
11130
|
+
* Is `managerId` PROVABLY a member of other organizations and not of
|
|
11131
|
+
* `organizationId`? (#10153)
|
|
11132
|
+
*
|
|
11133
|
+
* "Provably" is the whole shape of this screen, and it is deliberate rather
|
|
11134
|
+
* than a weaker version of "must prove membership":
|
|
11135
|
+
*
|
|
11136
|
+
* - membership rows exist for this user, none in the request's org
|
|
11137
|
+
* ⇒ the tenancy fact is present and NEGATIVE ⇒ screen him out;
|
|
11138
|
+
* - no membership rows at all, or the read failed
|
|
11139
|
+
* ⇒ the tenancy fact is ABSENT ⇒ leave routing exactly as it was.
|
|
11140
|
+
*
|
|
11141
|
+
* The fail-open half is not timidity, it is this file's ruled posture on
|
|
11142
|
+
* addressing paths, stated twice already: {@link filterApproversWhoCanRead}
|
|
11143
|
+
* refuses to empty a live slate on an infrastructure hiccup, and
|
|
11144
|
+
* {@link expandPositionUsers} carries "a step routing to nobody is worse than
|
|
11145
|
+
* one routing to a lapsed holder". It is also load-bearing in practice — a
|
|
11146
|
+
* stack that stamps an organization on its requests but does not materialize
|
|
11147
|
+
* `sys_member` rows would otherwise lose every manager approver at once,
|
|
11148
|
+
* which is a bigger behaviour change than the hole being closed. Measured:
|
|
11149
|
+
* this repo's own `type:manager` out-of-office fixture is such a stack.
|
|
11150
|
+
*
|
|
11151
|
+
* Screening the MANAGER only, before OOO delegation, is deliberate too: the
|
|
11152
|
+
* delegate arrives from `sys_approval_delegation`, whose rows already carry
|
|
11153
|
+
* (and are already filtered by) an `organization_id` in
|
|
11154
|
+
* {@link lookupActiveDelegation}. This card is about `sys_user.manager_id`.
|
|
11155
|
+
*/
|
|
11156
|
+
private managerIsProvablyOutsideOrg;
|
|
10880
11157
|
/**
|
|
10881
11158
|
* Out-of-office auto-skip (#1322 M1). Given an individually-routed approver
|
|
10882
11159
|
* id, follow any active `sys_approval_delegation` chain and return the id the
|
|
@@ -11408,6 +11685,56 @@ declare class ApprovalService implements IApprovalService {
|
|
|
11408
11685
|
inserted: number;
|
|
11409
11686
|
deleted: number;
|
|
11410
11687
|
}>;
|
|
11688
|
+
/**
|
|
11689
|
+
* [#11040] May the free-text pushdown carry an arm on the SNAPSHOT column,
|
|
11690
|
+
* for THIS caller over THIS query's scope?
|
|
11691
|
+
*
|
|
11692
|
+
* `payload_json` is the one searched column whose contents the serve path
|
|
11693
|
+
* masks per reader (`redactPayloads`, #10749). A predicate over it is
|
|
11694
|
+
* evaluated by the driver against the column AT REST — unmasked, before
|
|
11695
|
+
* anything is served — so for a caller whose view of the snapshot is masked,
|
|
11696
|
+
* row membership answers questions about contents that caller may not read.
|
|
11697
|
+
* The other four arms are columns of `sys_approval_request` itself, which
|
|
11698
|
+
* every caller who can see the row reads whole; they are untouched.
|
|
11699
|
+
*
|
|
11700
|
+
* ## The invariant this method exists to hold
|
|
11701
|
+
*
|
|
11702
|
+
* "This caller's view is masked" is read from **the same authority and the
|
|
11703
|
+
* same per-caller call as the serve path** — `resolveReadableSnapshotFields`,
|
|
11704
|
+
* asked as the CALLER (never `SYSTEM_CTX`). A second, independently derived
|
|
11705
|
+
* notion of "redacted" — comparing the readable set against the object's
|
|
11706
|
+
* schema, say — would be a fresh source of drift, and drift between the
|
|
11707
|
+
* serve rule and the filter rule IS the defect this closes, reconstituted one
|
|
11708
|
+
* layer down. So the only "not masked" answer accepted here is the one serve
|
|
11709
|
+
* itself acts on: `undefined`, the seam's documented do-not-narrow branch.
|
|
11710
|
+
* When the seam holds a concrete list the mask is IN FORCE, whether or not it
|
|
11711
|
+
* happens to remove a key from any particular row — a row-dependent question
|
|
11712
|
+
* no predicate can answer before rows exist.
|
|
11713
|
+
*
|
|
11714
|
+
* ## The two predicate-time cases
|
|
11715
|
+
*
|
|
11716
|
+
* Redaction is decided per ROW (each row names its own subject object, hence
|
|
11717
|
+
* its own readable set), but a filter is built before any row exists:
|
|
11718
|
+
*
|
|
11719
|
+
* - **authority absent** — `resolveReadableSnapshotFields` answers
|
|
11720
|
+
* `undefined` for EVERY object, so serve hands over every snapshot whole.
|
|
11721
|
+
* Keeping the arm leaks nothing serve does not already hand over, and this
|
|
11722
|
+
* is the shape every deployment that has not wired the security plugin
|
|
11723
|
+
* gets: search is byte-for-byte unchanged. Checked first, and that order
|
|
11724
|
+
* is load-bearing — see the object-scope note below.
|
|
11725
|
+
* - **authority wired** — the readable set is per object, so the arm is
|
|
11726
|
+
* admissible only for a scope of exactly one KNOWN object. With
|
|
11727
|
+
* `filter.object` present that object is known at predicate time and the
|
|
11728
|
+
* seam is asked about it directly. Absent, the query spans every object
|
|
11729
|
+
* and there is nothing sound to ask, so the arm is dropped.
|
|
11730
|
+
*
|
|
11731
|
+
* Dropping an arm is strictly NARROWING: it never refuses a query and never
|
|
11732
|
+
* widens what comes back, so the fail-closed direction is cheap here and is
|
|
11733
|
+
* taken rather than reaching for a per-object predicate machine to avoid it.
|
|
11734
|
+
* Refusing the query outright would be the louder behaviour change, and is
|
|
11735
|
+
* deliberately not what this does.
|
|
11736
|
+
*/
|
|
11737
|
+
private freeTextMayMatchSnapshot;
|
|
11411
11738
|
/** Filter type accepted by {@link listRequests} / {@link countRequests}. */
|
|
11412
11739
|
private buildRequestWhere;
|
|
11413
11740
|
/** Window the approver-index probe — pending queues live far below this. */
|
|
@@ -11641,10 +11968,45 @@ declare class ApprovalsServicePlugin implements Plugin {
|
|
|
11641
11968
|
private service?;
|
|
11642
11969
|
private engine?;
|
|
11643
11970
|
private escalationJobScheduled;
|
|
11971
|
+
/**
|
|
11972
|
+
* Captured where the job is scheduled, not resolved at teardown: `destroy()`
|
|
11973
|
+
* — the hook the kernel actually calls — takes NO `PluginContext`
|
|
11974
|
+
* (`Plugin.destroy?(): Promise<void> | void`, core `types.ts`). Holding the
|
|
11975
|
+
* very service the schedule was placed with also means the cancel cannot miss
|
|
11976
|
+
* it because the registry has already been torn down around us.
|
|
11977
|
+
*/
|
|
11978
|
+
private jobService?;
|
|
11644
11979
|
constructor(options?: ApprovalsPluginOptions);
|
|
11645
11980
|
init(ctx: PluginContext): Promise<void>;
|
|
11646
11981
|
start(ctx: PluginContext): Promise<void>;
|
|
11647
|
-
|
|
11982
|
+
/**
|
|
11983
|
+
* The kernel's teardown hook (`Plugin.destroy?()`, core `types.ts`) — the
|
|
11984
|
+
* ONLY teardown entry point `ObjectKernel.performShutdown()` and
|
|
11985
|
+
* `LiteKernel.destroy()` invoke.
|
|
11986
|
+
*
|
|
11987
|
+
* [#10371] IT USED TO BE `stop()`, WHICH NOTHING CALLED. `Plugin` declares
|
|
11988
|
+
* `init()`, `start?()` and `destroy?()` and no `stop()`, so the kernel walked
|
|
11989
|
+
* past this plugin at shutdown: the SLA escalation job stayed scheduled and
|
|
11990
|
+
* this plugin's ObjectQL hooks stayed bound to an engine the kernel had
|
|
11991
|
+
* finished with. `start()` IS on the interface, so the pair read as symmetric
|
|
11992
|
+
* in review — that asymmetry is what let the same shape survive in six
|
|
11993
|
+
* packages at once.
|
|
11994
|
+
*
|
|
11995
|
+
* This member owns no timer of its own (the escalation clock belongs to
|
|
11996
|
+
* `service-job`), so it never cost a merge-queue eviction the way the
|
|
11997
|
+
* `plugin-reports` / `service-messaging` members did (#9371). The class is
|
|
11998
|
+
* the same one either way: a teardown the kernel does not reach.
|
|
11999
|
+
*/
|
|
12000
|
+
destroy(): Promise<void>;
|
|
12001
|
+
/**
|
|
12002
|
+
* Retained alias for {@link destroy}. Kept because it is public API of an
|
|
12003
|
+
* exported class, and removing it would break an embedder who learned to call
|
|
12004
|
+
* it directly precisely BECAUSE the kernel never did. The parameter is now
|
|
12005
|
+
* optional and ignored: `destroy()` takes no context, so teardown uses the
|
|
12006
|
+
* job service captured when the escalation clock was wired. Prefer kernel
|
|
12007
|
+
* shutdown; direct callers keep working unchanged.
|
|
12008
|
+
*/
|
|
12009
|
+
stop(_ctx?: PluginContext): Promise<void>;
|
|
11648
12010
|
}
|
|
11649
12011
|
|
|
11650
12012
|
/** Minimal surface of the automation engine this provider depends on. */
|