@aglyn/aglyn 1.0.0-beta.285 → 1.0.0-beta.288
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/package.json +11 -11
- package/src/lib/app-utils/account-acquisition.d.ts +14 -0
- package/src/lib/app-utils/account-acquisition.js +19 -0
- package/src/lib/app-utils/account-acquisition.js.map +1 -1
- package/src/lib/app-utils/api-plugins.d.ts +25 -0
- package/src/lib/app-utils/api-plugins.js +13 -0
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/business-profile-values.d.ts +15 -3
- package/src/lib/app-utils/business-profile-values.js +19 -3
- package/src/lib/app-utils/business-profile-values.js.map +1 -1
- package/src/lib/app-utils/business-profile.d.ts +5 -3
- package/src/lib/app-utils/business-profile.js +2 -1
- package/src/lib/app-utils/business-profile.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +11 -0
- package/src/lib/app-utils/contacts.js +18 -0
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/docs-deflection.d.ts +1 -1
- package/src/lib/app-utils/docs-deflection.js +17 -0
- package/src/lib/app-utils/docs-deflection.js.map +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js +5 -2
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +9 -9
- package/src/lib/app-utils/docs-help.generated.js +20 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +168 -63
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/docs-retrieval.d.ts +17 -0
- package/src/lib/app-utils/docs-retrieval.js +28 -0
- package/src/lib/app-utils/docs-retrieval.js.map +1 -1
- package/src/lib/app-utils/host-naming.d.ts +2 -1
- package/src/lib/app-utils/host-naming.js +65 -2
- package/src/lib/app-utils/host-naming.js.map +1 -1
- package/src/lib/app-utils/host-permissions.d.ts +159 -3
- package/src/lib/app-utils/host-permissions.js +384 -12
- package/src/lib/app-utils/host-permissions.js.map +1 -1
- package/src/lib/app-utils/org-permission-editor.d.ts +128 -1
- package/src/lib/app-utils/org-permission-editor.js +176 -1
- package/src/lib/app-utils/org-permission-editor.js.map +1 -1
- package/src/lib/app-utils/org-permissions.d.ts +62 -1
- package/src/lib/app-utils/org-permissions.js +55 -5
- package/src/lib/app-utils/org-permissions.js.map +1 -1
- package/src/lib/app-utils/org-role-grants.d.ts +28 -0
- package/src/lib/app-utils/org-role-grants.js +180 -2
- package/src/lib/app-utils/org-role-grants.js.map +1 -1
- package/src/lib/app-utils/org-roles.js +11 -2
- package/src/lib/app-utils/org-roles.js.map +1 -1
- package/src/lib/app-utils/redirect-query.d.ts +39 -0
- package/src/lib/app-utils/redirect-query.js +57 -0
- package/src/lib/app-utils/redirect-query.js.map +1 -0
- package/src/lib/app-utils/release-flags.js +2 -2
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/site-starter-links.d.ts +42 -0
- package/src/lib/app-utils/site-starter-links.js +79 -0
- package/src/lib/app-utils/site-starter-links.js.map +1 -0
- package/src/lib/app-utils/site-starter-nodes.d.ts +91 -0
- package/src/lib/app-utils/site-starter-nodes.js +210 -0
- package/src/lib/app-utils/site-starter-nodes.js.map +1 -0
- package/src/lib/app-utils/site-starter-theme.d.ts +90 -0
- package/src/lib/app-utils/site-starter-theme.js +241 -0
- package/src/lib/app-utils/site-starter-theme.js.map +1 -0
- package/src/lib/app-utils/webhook-delivery.js +6 -1
- package/src/lib/app-utils/webhook-delivery.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +12 -2
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +39 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-closure-notices.d.ts +88 -0
- package/src/lib/plugin-manager/plugin-closure-notices.js +123 -0
- package/src/lib/plugin-manager/plugin-closure-notices.js.map +1 -0
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +15 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js +24 -1
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-customer-display-verifier.d.ts +77 -0
- package/src/lib/plugin-manager/plugin-customer-display-verifier.js +74 -0
- package/src/lib/plugin-manager/plugin-customer-display-verifier.js.map +1 -0
- package/src/lib/plugin-manager/plugin-customer-display.d.ts +135 -0
- package/src/lib/plugin-manager/plugin-customer-display.js +189 -0
- package/src/lib/plugin-manager/plugin-customer-display.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +59 -0
- package/src/lib/plugin-manager/plugin-entitlements.js +32 -3
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/plugin-events.d.ts +45 -0
- package/src/lib/plugin-manager/plugin-events.js.map +1 -1
- package/src/lib/plugin-manager/plugin-permissions.d.ts +74 -0
- package/src/lib/plugin-manager/plugin-permissions.js +91 -1
- package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-starters.d.ts +7 -0
- package/src/lib/plugin-manager/plugin-site-starters.js.map +1 -1
- package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +87 -4
- package/src/lib/plugin-manager/plugin-tax-return-sources.js +155 -35
- package/src/lib/plugin-manager/plugin-tax-return-sources.js.map +1 -1
|
@@ -13,9 +13,12 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { refinePermissionMap } from "../plugin-manager/plugin-permissions.js";
|
|
17
18
|
import { hostRoleFor, isOrgWideMember } from "./organizations.js";
|
|
18
|
-
import {
|
|
19
|
+
import { CORE_SITE_ROLE_PRESETS } from "./org-role-grants.js";
|
|
20
|
+
import { permissionRefusalText, ORG_PERMISSION_REFINEMENTS, ORG_PERMISSIONS, resolveOrgPermissions } from "./org-permissions.js";
|
|
21
|
+
import { orgRoleTier } from "./org-roles.js";
|
|
19
22
|
/**
|
|
20
23
|
* Catalog keys a site collaborator holds PER SITE (AGL-2984), resolved on one
|
|
21
24
|
* of two axes.
|
|
@@ -35,14 +38,23 @@ import { resolveOrgPermissions } from "./org-permissions.js";
|
|
|
35
38
|
* names no site at all: there is no host role to read a default from, and
|
|
36
39
|
* guessing one would let a request reach around the per-site toggle by
|
|
37
40
|
* omitting the site.
|
|
38
|
-
*/ /**
|
|
39
|
-
|
|
41
|
+
*/ /**
|
|
42
|
+
* The catalog keys a collaborator can hold per site, in catalog order: every
|
|
43
|
+
* key with host-role defaults, the core's site keys (AGL-4047) and the ones
|
|
44
|
+
* plugins declare.
|
|
45
|
+
*/ export function hostPermissionKeys() {
|
|
46
|
+
return ORG_PERMISSIONS.filter((definition)=>definition.hostRoleDefaults !== undefined).map((definition)=>definition.key);
|
|
40
47
|
}
|
|
41
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* What a host role grants a site collaborator for every per-site key, before
|
|
50
|
+
* any toggle. A key whose site-role preset holds for members of the whole
|
|
51
|
+
* workspace only (`workspaceMembersOnly`, AGL-4047) grants a collaborator
|
|
52
|
+
* nothing, whatever their site role.
|
|
53
|
+
*/ export function hostRolePermissionDefaults(role) {
|
|
42
54
|
const defaults = {};
|
|
43
|
-
for (const
|
|
44
|
-
if (!
|
|
45
|
-
defaults[
|
|
55
|
+
for (const definition of ORG_PERMISSIONS){
|
|
56
|
+
if (!definition.hostRoleDefaults) continue;
|
|
57
|
+
defaults[definition.key] = !definition.workspaceMembersOnly && definition.hostRoleDefaults[role] === true;
|
|
46
58
|
}
|
|
47
59
|
return defaults;
|
|
48
60
|
}
|
|
@@ -52,6 +64,13 @@ import { resolveOrgPermissions } from "./org-permissions.js";
|
|
|
52
64
|
false
|
|
53
65
|
]));
|
|
54
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The catalog's refinements whose key and broader key are both in `map`
|
|
69
|
+
* (AGL-4047): the links a per-site map can settle on its own. A link to a
|
|
70
|
+
* key the map does not carry would read that key as refused and add it.
|
|
71
|
+
*/ function refinementsWithin(map) {
|
|
72
|
+
return ORG_PERMISSION_REFINEMENTS.filter(([key, parent])=>key in map && parent in map);
|
|
73
|
+
}
|
|
55
74
|
/**
|
|
56
75
|
* A collaborator's verdict on ONE site for every per-site key, or null where
|
|
57
76
|
* they have no access to that site at all. An absent toggle reads as the host
|
|
@@ -70,7 +89,314 @@ import { resolveOrgPermissions } from "./org-permissions.js";
|
|
|
70
89
|
if (typeof value === 'boolean') merged[key] = value;
|
|
71
90
|
}
|
|
72
91
|
}
|
|
73
|
-
|
|
92
|
+
// A per-site key that follows or refines another per-site key (AGL-4047)
|
|
93
|
+
// takes its verdict from that key on this site where the site sets nothing
|
|
94
|
+
// for it, as the org catalog's resolver does with a custom role.
|
|
95
|
+
return refinePermissionMap(merged, refinementsWithin(merged), [
|
|
96
|
+
toggles
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
/*
|
|
100
|
+
* KEYS WHOSE PRESET IS THE SITE ROLE (AGL-4047).
|
|
101
|
+
*
|
|
102
|
+
* Some checks were written against the member's role on a site: who may book
|
|
103
|
+
* a walk-in, change a stay or edit a site's reservations asked whether their
|
|
104
|
+
* `memberRoles` entry was Admin, Editor or Author. Roles are only presets of
|
|
105
|
+
* permissions, so those checks became keys, and a key that replaces a gate
|
|
106
|
+
* on the site role keeps that role as its preset (`presetFromSiteRole`):
|
|
107
|
+
*
|
|
108
|
+
* 1. the member's role ON THE SITE (`hostRoleFor`): Admin for the owner and
|
|
109
|
+
* admins, a workspace-wide editor's or viewer's own role unless the site
|
|
110
|
+
* gives them other access there, narrower or wider, and a collaborator's
|
|
111
|
+
* site role; no access to the site holds nothing;
|
|
112
|
+
* 2. that role's `hostRoleDefaults` verdict for the key;
|
|
113
|
+
* 3. then, for a workspace-wide member, their custom role's verdict and
|
|
114
|
+
* their own permissions' verdict, when either sets the key;
|
|
115
|
+
* 4. then the site's own toggle for them (`hostPermissions[hostId][key]`).
|
|
116
|
+
*
|
|
117
|
+
* With nothing set at 3 or 4, the verdict is exactly what the role gate
|
|
118
|
+
* answered, for every member on every site, which is how the built-in roles
|
|
119
|
+
* keep the access they had. A collaborator has no custom role and no
|
|
120
|
+
* workspace standing to refine, so only the site's toggle moves their
|
|
121
|
+
* verdict, as for every other per-site key.
|
|
122
|
+
*
|
|
123
|
+
* A key declared `workspaceMembersOnly` replaces a gate that asked for an
|
|
124
|
+
* admin of the whole workspace who is also this site's admin: its preset at
|
|
125
|
+
* 2 is the site role's for a member of the whole workspace and refusal for a
|
|
126
|
+
* collaborator, whose site role (even Admin) never carried it. The site's
|
|
127
|
+
* toggle at 4 still grants it to them.
|
|
128
|
+
*
|
|
129
|
+
* These keys narrow nothing (`narrowsPerSite` is dropped): the site role is
|
|
130
|
+
* already their preset. With no site named, the org catalog's verdict
|
|
131
|
+
* (`roleDefaults`, then the layers) stands, which is the same answer for a
|
|
132
|
+
* workspace-wide member on a site where their access is their workspace role.
|
|
133
|
+
*/ /** The catalog keys whose preset is the member's role on the site, in catalog order. */ export function siteRolePresetPermissionKeys() {
|
|
134
|
+
return ORG_PERMISSIONS.filter((definition)=>definition.presetFromSiteRole).map((definition)=>definition.key);
|
|
135
|
+
}
|
|
136
|
+
/** Is this a key whose preset is the member's role on the site? */ export function isSiteRolePresetPermission(permission) {
|
|
137
|
+
return ORG_PERMISSIONS.some((definition)=>definition.key === permission && definition.presetFromSiteRole === true);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A member's verdict on ONE site for every key whose preset is their role
|
|
141
|
+
* there (see above). `customRole` is their custom role document when they
|
|
142
|
+
* carry a `roleId` and the caller has it, and is ignored for a collaborator.
|
|
143
|
+
* No site named, or no access to it, refuses every such key.
|
|
144
|
+
*/ export function resolveSiteRolePresetPermissions(member, hostId, customRole) {
|
|
145
|
+
var _member_hostPermissions;
|
|
146
|
+
const definitions = ORG_PERMISSIONS.filter((definition)=>definition.presetFromSiteRole);
|
|
147
|
+
if (!definitions.length) return {};
|
|
148
|
+
const role = member && hostId ? hostRoleFor(member, hostId) : null;
|
|
149
|
+
if (!member || !hostId || !role) {
|
|
150
|
+
return Object.fromEntries(definitions.map((definition)=>[
|
|
151
|
+
definition.key,
|
|
152
|
+
false
|
|
153
|
+
]));
|
|
154
|
+
}
|
|
155
|
+
const toggles = (_member_hostPermissions = member.hostPermissions) == null ? void 0 : _member_hostPermissions[hostId];
|
|
156
|
+
const workspaceMember = isOrgWideMember(member);
|
|
157
|
+
const layers = workspaceMember ? [
|
|
158
|
+
member.roleId ? customRole == null ? void 0 : customRole.permissions : undefined,
|
|
159
|
+
member.permissions,
|
|
160
|
+
toggles
|
|
161
|
+
] : [
|
|
162
|
+
toggles
|
|
163
|
+
];
|
|
164
|
+
const out = {};
|
|
165
|
+
for (const definition of definitions){
|
|
166
|
+
var _definition_hostRoleDefaults;
|
|
167
|
+
let verdict = (workspaceMember || !definition.workspaceMembersOnly) && ((_definition_hostRoleDefaults = definition.hostRoleDefaults) == null ? void 0 : _definition_hostRoleDefaults[role]) === true;
|
|
168
|
+
for (const layer of layers){
|
|
169
|
+
const value = layer == null ? void 0 : layer[definition.key];
|
|
170
|
+
if (typeof value === 'boolean') verdict = value;
|
|
171
|
+
}
|
|
172
|
+
out[definition.key] = verdict;
|
|
173
|
+
}
|
|
174
|
+
return refinePermissionMap(out, refinementsWithin(out), layers);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* `granted` with every key whose preset is the site role decided for this
|
|
178
|
+
* site. With no site named it is `granted` as given, so a resolver can call
|
|
179
|
+
* it unconditionally.
|
|
180
|
+
*/ export function applySiteRolePresets(granted, member, hostId, customRole) {
|
|
181
|
+
if (!hostId) return granted;
|
|
182
|
+
return _extends({}, granted, resolveSiteRolePresetPermissions(member, hostId, customRole));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The verdict the site's own stamp gives a member for one key whose preset
|
|
186
|
+
* is the site role, as the Firestore rules read it (AGL-4047): the site's
|
|
187
|
+
* `memberPermissions[uid][key]` when the projection wrote one, else the
|
|
188
|
+
* preset of the role the site's `memberRoles` projection holds for them.
|
|
189
|
+
*
|
|
190
|
+
* For a door that has the host document in hand and asks what the rules
|
|
191
|
+
* ask, so the two cannot disagree: the stamp is written beside
|
|
192
|
+
* `memberRoles` by the same projection, with every layer applied, and a
|
|
193
|
+
* site the projection has not reached since the key existed answers exactly
|
|
194
|
+
* as the role gate it replaced. `presetFor` is the declaring plugin's own
|
|
195
|
+
* table, so the answer does not depend on which plugins this process loaded.
|
|
196
|
+
*/ export function hostStampedSitePermission(host, uid, permission, presetFor) {
|
|
197
|
+
var _host_memberPermissions_uid, _host_memberPermissions, _host_memberRoles;
|
|
198
|
+
const stamped = host == null ? void 0 : (_host_memberPermissions = host.memberPermissions) == null ? void 0 : (_host_memberPermissions_uid = _host_memberPermissions[uid]) == null ? void 0 : _host_memberPermissions_uid[permission];
|
|
199
|
+
if (typeof stamped === 'boolean') return stamped;
|
|
200
|
+
const role = host == null ? void 0 : (_host_memberRoles = host.memberRoles) == null ? void 0 : _host_memberRoles[uid];
|
|
201
|
+
return typeof role === 'string' && presetFor[role] === true;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The same for a key whose preset holds for members of the whole workspace
|
|
205
|
+
* only (`workspaceMembersOnly`, AGL-4047), which `memberRoles` alone cannot
|
|
206
|
+
* answer: an admin of one site and an admin of the whole workspace project
|
|
207
|
+
* the same `'admin'` there. The site's stamp when the projection wrote one;
|
|
208
|
+
* else the preset of the role `memberRoles` holds for them AND of the role
|
|
209
|
+
* the fresh resolution gives them on the site, for a member of the whole
|
|
210
|
+
* workspace only. Exactly the gate these keys replaced: the projection's
|
|
211
|
+
* role, `orgWide` and the fresh site role, all three.
|
|
212
|
+
*/ export function hostStampedWorkspaceSitePermission(host, uid, permission, presetFor, membership) {
|
|
213
|
+
var _host_memberPermissions_uid, _host_memberPermissions;
|
|
214
|
+
const stamped = host == null ? void 0 : (_host_memberPermissions = host.memberPermissions) == null ? void 0 : (_host_memberPermissions_uid = _host_memberPermissions[uid]) == null ? void 0 : _host_memberPermissions_uid[permission];
|
|
215
|
+
if (typeof stamped === 'boolean') return stamped;
|
|
216
|
+
if (membership.orgWide !== true) return false;
|
|
217
|
+
const fresh = membership.hostRole;
|
|
218
|
+
return typeof fresh === 'string' && presetFor[fresh] === true && hostStampedSitePermission({
|
|
219
|
+
memberRoles: host == null ? void 0 : host.memberRoles
|
|
220
|
+
}, uid, permission, presetFor);
|
|
221
|
+
}
|
|
222
|
+
/*
|
|
223
|
+
* A SITE NARROWS A WORKSPACE-WIDE MEMBER (AGL-4047).
|
|
224
|
+
*
|
|
225
|
+
* An editor who works across the workspace can be given Viewer or Author
|
|
226
|
+
* access to one site (`hostAccess[hostId]`, which `hostRoleFor` reads ahead
|
|
227
|
+
* of `allHosts`). That narrowed their content rights there and nothing else,
|
|
228
|
+
* so a key the workspace grants an editor, such as working a site's
|
|
229
|
+
* register, still held on the site they were narrowed on.
|
|
230
|
+
*
|
|
231
|
+
* A plugin marks the keys a site CAN narrow with `narrowsPerSite`, and
|
|
232
|
+
* whether each one does is a control with three layers, the nearest set one
|
|
233
|
+
* winning (`siteNarrowingChoices`):
|
|
234
|
+
*
|
|
235
|
+
* - the member's own choice (`member.siteNarrowing[key]`);
|
|
236
|
+
* - else their custom role's (`customRole.siteNarrowing[key]`);
|
|
237
|
+
* - else the built-in role's default the declaration names.
|
|
238
|
+
*
|
|
239
|
+
* `true` is "Follow the site role", `false` is "Keep this permission". On a
|
|
240
|
+
* site that narrows the member, each key that follows the site role is
|
|
241
|
+
* decided for that site:
|
|
242
|
+
*
|
|
243
|
+
* 1. the site's own verdict for the key (`hostPermissions[hostId][key]`),
|
|
244
|
+
* when the site set one;
|
|
245
|
+
* 2. else, for a key that refines another key that also follows the site
|
|
246
|
+
* role, that key's verdict on this site, never more than the workspace
|
|
247
|
+
* grants;
|
|
248
|
+
* 3. else the narrower access's default for the key, never more than the
|
|
249
|
+
* workspace grants;
|
|
250
|
+
*
|
|
251
|
+
* and a key they keep is the workspace's verdict, as everywhere else. A finer
|
|
252
|
+
* key they hold on the site (one the site grants, or one they keep and the
|
|
253
|
+
* workspace grants) brings the key it refines on this site with it, unless
|
|
254
|
+
* the site refuses that key, because a refining key is never held without
|
|
255
|
+
* it. The other finer keys do not follow that implied grant: allowing "sell"
|
|
256
|
+
* on a site is not allowing refunds.
|
|
257
|
+
*
|
|
258
|
+
* A member whose access to every site matches their workspace role (the
|
|
259
|
+
* owner and admins always do), one who keeps every key a site can narrow,
|
|
260
|
+
* and a site collaborator, whose keys come from their site role already, all
|
|
261
|
+
* resolve exactly as before.
|
|
262
|
+
*/ /** Host roles from the widest down, for "is this site's access narrower?". */ const HOST_ROLE_RANK = {
|
|
263
|
+
admin: 3,
|
|
264
|
+
editor: 2,
|
|
265
|
+
author: 1,
|
|
266
|
+
viewer: 0
|
|
267
|
+
};
|
|
268
|
+
/** The catalog keys a site CAN narrow (`narrowsPerSite`), in catalog order. */ export function siteNarrowedPermissionKeys() {
|
|
269
|
+
return ORG_PERMISSIONS.filter((definition)=>definition.narrowsPerSite).map((definition)=>definition.key);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Could any site narrow a member of this org role? Not the owner or an
|
|
273
|
+
* admin, who are admins of every site, and not a role whose site access has
|
|
274
|
+
* nothing narrower below it (a viewer). The editors' control is the one that
|
|
275
|
+
* changes anything; the role editor says so beside it.
|
|
276
|
+
*/ export function roleCanBeNarrowed(role) {
|
|
277
|
+
if (role === 'owner' || role === 'admin') return false;
|
|
278
|
+
return HOST_ROLE_RANK[orgRoleTier(role)] > 0;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Whether each key a site can narrow follows the site role for this member
|
|
282
|
+
* (`true`) or is kept (`false`): their own choice, else their custom role's
|
|
283
|
+
* (when they carry its id and it was supplied), else the built-in role's
|
|
284
|
+
* default.
|
|
285
|
+
*/ export function siteNarrowingChoices(member, customRole) {
|
|
286
|
+
var _ref, _ref1, _ref2;
|
|
287
|
+
const role = (_ref = member == null ? void 0 : member.role) != null ? _ref : 'viewer';
|
|
288
|
+
const own = (_ref1 = member == null ? void 0 : member.siteNarrowing) != null ? _ref1 : {};
|
|
289
|
+
const fromRole = (member == null ? void 0 : member.roleId) ? (_ref2 = customRole == null ? void 0 : customRole.siteNarrowing) != null ? _ref2 : {} : {};
|
|
290
|
+
const out = {};
|
|
291
|
+
for (const definition of ORG_PERMISSIONS){
|
|
292
|
+
if (!definition.narrowsPerSite) continue;
|
|
293
|
+
const key = definition.key;
|
|
294
|
+
const mine = own[key];
|
|
295
|
+
const theirRole = fromRole[key];
|
|
296
|
+
out[key] = typeof mine === 'boolean' ? mine : typeof theirRole === 'boolean' ? theirRole : definition.narrowsPerSite[role] === true;
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Is this member's access to this site narrower than their workspace role?
|
|
302
|
+
* The cheap half of `siteNarrowingFor`, which needs no custom role: a caller
|
|
303
|
+
* that has to read one asks this first, and reads nothing for everyone else.
|
|
304
|
+
*/ export function siteMayNarrow(member, hostId) {
|
|
305
|
+
if (!member || !hostId || !isOrgWideMember(member)) return null;
|
|
306
|
+
const role = hostRoleFor(member, hostId);
|
|
307
|
+
if (!role) return null;
|
|
308
|
+
const workspaceRole = orgRoleTier(member.role);
|
|
309
|
+
if (HOST_ROLE_RANK[role] >= HOST_ROLE_RANK[workspaceRole]) return null;
|
|
310
|
+
return role;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* How this site narrows this member, or null where it does not: a member
|
|
314
|
+
* who is not workspace-wide, has no access to the site, whose access there
|
|
315
|
+
* is as wide as their workspace role (owners and admins always), or who
|
|
316
|
+
* keeps every key a site can narrow. `customRole` is their custom role
|
|
317
|
+
* document when they carry a `roleId`; without it, only their own choices
|
|
318
|
+
* and the built-in defaults decide.
|
|
319
|
+
*/ export function siteNarrowingFor(member, hostId, customRole) {
|
|
320
|
+
var _ref;
|
|
321
|
+
var _member_hostPermissions;
|
|
322
|
+
const role = siteMayNarrow(member, hostId);
|
|
323
|
+
if (!role) return null;
|
|
324
|
+
const narrows = siteNarrowingChoices(member, customRole);
|
|
325
|
+
if (!Object.values(narrows).some(Boolean)) return null;
|
|
326
|
+
return {
|
|
327
|
+
role,
|
|
328
|
+
grants: (_ref = member == null ? void 0 : (_member_hostPermissions = member.hostPermissions) == null ? void 0 : _member_hostPermissions[hostId]) != null ? _ref : {},
|
|
329
|
+
narrows
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/** The keys that follow the site role under a narrowing, in catalog order; none without one. */ export function narrowedPermissionKeysOf(narrowing) {
|
|
333
|
+
if (!narrowing) return [];
|
|
334
|
+
return siteNarrowedPermissionKeys().filter((key)=>narrowing.narrows[key] === true);
|
|
335
|
+
}
|
|
336
|
+
/** What a site access holds by default for a key: its site default, else its tier's (Author on the Viewer tier). */ function narrowedDefault(definition, role) {
|
|
337
|
+
var _definition_hostRoleDefaults;
|
|
338
|
+
const site = (_definition_hostRoleDefaults = definition.hostRoleDefaults) == null ? void 0 : _definition_hostRoleDefaults[role];
|
|
339
|
+
if (typeof site === 'boolean') return site;
|
|
340
|
+
return definition.roleDefaults[orgRoleTier(role)] === true;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* A workspace-wide member's resolved map with the keys that follow the site
|
|
344
|
+
* role decided for that site (see above). With no narrowing it is the map
|
|
345
|
+
* as given, so every resolver can call it unconditionally.
|
|
346
|
+
*/ export function narrowPermissionsToSite(granted, narrowing) {
|
|
347
|
+
if (!narrowing) return granted;
|
|
348
|
+
const definitions = ORG_PERMISSIONS.filter((definition)=>definition.narrowsPerSite);
|
|
349
|
+
if (!definitions.length) return granted;
|
|
350
|
+
const narrowable = new Set(definitions.map((definition)=>definition.key));
|
|
351
|
+
const follows = (key)=>narrowing.narrows[key] === true;
|
|
352
|
+
// A finer key's broader key, among the keys a site can narrow.
|
|
353
|
+
const broader = new Map();
|
|
354
|
+
for (const [key, parent, kind] of ORG_PERMISSION_REFINEMENTS){
|
|
355
|
+
if (kind !== 'follows' && narrowable.has(key) && narrowable.has(parent)) broader.set(key, parent);
|
|
356
|
+
}
|
|
357
|
+
const siteVerdict = (key)=>{
|
|
358
|
+
const value = narrowing.grants[key];
|
|
359
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
360
|
+
};
|
|
361
|
+
const depth = (key)=>{
|
|
362
|
+
const seen = new Set();
|
|
363
|
+
let steps = 0;
|
|
364
|
+
for(let current = key; broader.has(current) && !seen.has(current); steps += 1){
|
|
365
|
+
seen.add(current);
|
|
366
|
+
current = broader.get(current);
|
|
367
|
+
}
|
|
368
|
+
return steps;
|
|
369
|
+
};
|
|
370
|
+
const out = _extends({}, granted);
|
|
371
|
+
for (const definition of [
|
|
372
|
+
...definitions
|
|
373
|
+
].sort((a, b)=>depth(a.key) - depth(b.key))){
|
|
374
|
+
const key = definition.key;
|
|
375
|
+
const workspace = granted[key] === true;
|
|
376
|
+
// Kept: the workspace's verdict, as on every other site.
|
|
377
|
+
if (!follows(key)) {
|
|
378
|
+
out[key] = workspace;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const own = siteVerdict(key);
|
|
382
|
+
const parent = broader.get(key);
|
|
383
|
+
if (own !== undefined) out[key] = own;
|
|
384
|
+
else if (parent && follows(parent)) out[key] = out[parent] === true && workspace;
|
|
385
|
+
else out[key] = workspace && narrowedDefault(definition, narrowing.role);
|
|
386
|
+
}
|
|
387
|
+
for (const definition of definitions){
|
|
388
|
+
const key = definition.key;
|
|
389
|
+
const held = follows(key) ? siteVerdict(key) === true : granted[key] === true;
|
|
390
|
+
if (!held) continue;
|
|
391
|
+
const seen = new Set();
|
|
392
|
+
for(let parent = broader.get(key); parent && !seen.has(parent); parent = broader.get(parent)){
|
|
393
|
+
seen.add(parent);
|
|
394
|
+
// A kept broader key is the workspace's verdict already.
|
|
395
|
+
if (!follows(parent) || siteVerdict(parent) === false) break;
|
|
396
|
+
out[parent] = true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return refinePermissionMap(out, ORG_PERMISSION_REFINEMENTS);
|
|
74
400
|
}
|
|
75
401
|
/**
|
|
76
402
|
* A member's verdict for every per-site key, on whichever axis their
|
|
@@ -80,14 +406,26 @@ import { resolveOrgPermissions } from "./org-permissions.js";
|
|
|
80
406
|
* `roleId` and the caller has it in hand, and is ignored for a collaborator,
|
|
81
407
|
* whose org role is a seat-accounting fact rather than a grant. `hostId` is
|
|
82
408
|
* the site the request is about; an org-wide member resolves the same on
|
|
83
|
-
* every site and with none named.
|
|
409
|
+
* every site and with none named, except on a site that narrows them.
|
|
410
|
+
*
|
|
411
|
+
* An org-wide member's map also carries every key a site can narrow
|
|
412
|
+
* (AGL-4047), decided for `hostId`, so the site's `memberPermissions`
|
|
413
|
+
* projection answers the rules what the server answers. A collaborator's
|
|
414
|
+
* map does not: their register and waivers keys come from their site role
|
|
415
|
+
* and their own overrides, which the rules read from their member document
|
|
416
|
+
* as before.
|
|
84
417
|
*/ export function resolveMemberHostPermissions(options) {
|
|
85
418
|
var _resolveCollaboratorHostPermissions;
|
|
86
419
|
const { member, customRole, hostId } = options;
|
|
87
420
|
if (!member) return noHostPermissions();
|
|
88
421
|
if (isOrgWideMember(member)) {
|
|
89
|
-
|
|
90
|
-
|
|
422
|
+
// The keys whose preset is the site role (AGL-4047) are decided for this
|
|
423
|
+
// site last; they narrow nothing, so the narrowing pass leaves them be.
|
|
424
|
+
const granted = applySiteRolePresets(narrowPermissionsToSite(resolveOrgPermissions(member, customRole != null ? customRole : null), siteNarrowingFor(member, hostId, customRole != null ? customRole : null)), member, hostId, customRole != null ? customRole : null);
|
|
425
|
+
return Object.fromEntries([
|
|
426
|
+
...hostPermissionKeys(),
|
|
427
|
+
...siteNarrowedPermissionKeys()
|
|
428
|
+
].map((key)=>[
|
|
91
429
|
key,
|
|
92
430
|
granted[key] === true
|
|
93
431
|
]));
|
|
@@ -115,5 +453,39 @@ import { resolveOrgPermissions } from "./org-permissions.js";
|
|
|
115
453
|
}
|
|
116
454
|
return projection;
|
|
117
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Does this member hold one of the core's site keys on this site (AGL-4047)?
|
|
458
|
+
* The door's twin of the rules' `canWriteHostContent`,
|
|
459
|
+
* `canPublishHostContent`, `canReadHostSensitive` and site-settings checks:
|
|
460
|
+
* the site's `memberPermissions` stamp, else the preset of the member's
|
|
461
|
+
* `memberRoles` entry (`hostStampedSitePermission`), so a route that asks it
|
|
462
|
+
* answers what the rules answer for the same write. For the routes that read
|
|
463
|
+
* the host document already; it costs no read.
|
|
464
|
+
*/ export function holdsSiteKey(hostDoc, uid, key) {
|
|
465
|
+
var _CORE_SITE_ROLE_PRESETS_key;
|
|
466
|
+
return holdsStampedSiteKey(hostDoc, uid, key, (_CORE_SITE_ROLE_PRESETS_key = CORE_SITE_ROLE_PRESETS[key]) != null ? _CORE_SITE_ROLE_PRESETS_key : {});
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* The same for a plugin's key whose preset is the site role (AGL-4047): the
|
|
470
|
+
* site's `memberPermissions` stamp, else `presetFor` read for the member's
|
|
471
|
+
* `memberRoles` entry. `presetFor` is the declaring plugin's own table, so the
|
|
472
|
+
* answer does not depend on which plugins this process loaded. For the doors
|
|
473
|
+
* and the cards that hold the host document already; it costs no read.
|
|
474
|
+
*/ export function holdsStampedSiteKey(hostDoc, uid, key, presetFor) {
|
|
475
|
+
if (!hostDoc || !uid) return false;
|
|
476
|
+
// `hostDoc`, not `host`: the host-field deny guard reads every `host.…`
|
|
477
|
+
// in this folder as a field of the host document, and `get` is none.
|
|
478
|
+
const read = (field)=>typeof hostDoc.get === 'function' ? hostDoc.get(field) : hostDoc[field];
|
|
479
|
+
return hostStampedSitePermission({
|
|
480
|
+
memberPermissions: read('memberPermissions'),
|
|
481
|
+
memberRoles: read('memberRoles')
|
|
482
|
+
}, uid, key, presetFor);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* The refusal a door answers when a member does not hold a site key
|
|
486
|
+
* (AGL-4047): it names the permission and who can change it, never a role.
|
|
487
|
+
*/ export function siteKeyRefusal(key) {
|
|
488
|
+
return permissionRefusalText(key);
|
|
489
|
+
}
|
|
118
490
|
|
|
119
491
|
//# sourceMappingURL=host-permissions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-permissions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n AglynOrgMember,\n HostAccessRole,\n HostPermissionKey,\n HostUid,\n} from '../foundation'\nimport { listPluginOrgPermissions } from '../plugin-manager/plugin-entitlements'\nimport { hostRoleFor, isOrgWideMember } from './organizations'\nimport { resolveOrgPermissions, type AglynOrgCustomRole } from './org-permissions'\n\n/**\n * Catalog keys a site collaborator holds PER SITE (AGL-2984), resolved on one\n * of two axes.\n *\n * A plugin that declares a catalog key with `hostRoleDefaults` makes it a\n * per-site key. For the people whose membership spans the workspace, the key\n * is an ordinary catalog key: their org role's default, refined by a custom\n * role and by per-member overrides, the same on every site. A site\n * collaborator has no org standing to refine — their membership exists only\n * to carry access to named sites — so for them the key is decided PER SITE,\n * from the host role they hold there, refined by `hostPermissions[hostId]` on\n * their member document.\n *\n * Which axis applies is `isOrgWideMember`, the predicate the rules projection\n * and the scope resolver already share. A collaborator asked about a site\n * they were never granted resolves to nothing, and so does one whose request\n * names no site at all: there is no host role to read a default from, and\n * guessing one would let a request reach around the per-site toggle by\n * omitting the site.\n */\n\n/** The catalog keys a collaborator can hold per site, in catalog order. */\nexport function hostPermissionKeys(): HostPermissionKey[] {\n return listPluginOrgPermissions()\n .filter((permission) => permission.hostRoleDefaults !== undefined)\n .map((permission) => permission.key)\n}\n\n/** What a host role grants for every per-site key, before any toggle. */\nexport function hostRolePermissionDefaults(\n role: HostAccessRole,\n): Record<HostPermissionKey, boolean> {\n const defaults: Record<HostPermissionKey, boolean> = {}\n for (const permission of listPluginOrgPermissions()) {\n if (!permission.hostRoleDefaults) continue\n defaults[permission.key] = permission.hostRoleDefaults[role] === true\n }\n return defaults\n}\n\n/** Every per-site key, refused. */\nfunction noHostPermissions(): Record<HostPermissionKey, boolean> {\n return Object.fromEntries(hostPermissionKeys().map((key) => [key, false]))\n}\n\n/**\n * A collaborator's verdict on ONE site for every per-site key, or null where\n * they have no access to that site at all. An absent toggle reads as the host\n * role's default, which is what makes a member document written before\n * `hostPermissions` existed resolve exactly as it did before.\n */\nexport function resolveCollaboratorHostPermissions(\n member: Partial<AglynOrgMember> | null | undefined,\n hostId: HostUid | null | undefined,\n): Record<HostPermissionKey, boolean> | null {\n if (!member || !hostId) return null\n const role = hostRoleFor(member, hostId)\n if (!role) return null\n const merged = hostRolePermissionDefaults(role)\n const toggles = member.hostPermissions?.[hostId]\n if (toggles) {\n for (const key of Object.keys(merged)) {\n const value = toggles[key]\n if (typeof value === 'boolean') merged[key] = value\n }\n }\n return merged\n}\n\n/**\n * A member's verdict for every per-site key, on whichever axis their\n * membership is on.\n *\n * `customRole` is the member's custom role document when they carry a\n * `roleId` and the caller has it in hand, and is ignored for a collaborator,\n * whose org role is a seat-accounting fact rather than a grant. `hostId` is\n * the site the request is about; an org-wide member resolves the same on\n * every site and with none named.\n */\nexport function resolveMemberHostPermissions(options: {\n member: Partial<AglynOrgMember> | null | undefined\n customRole?: AglynOrgCustomRole | null\n hostId?: HostUid | null\n}): Record<HostPermissionKey, boolean> {\n const { member, customRole, hostId } = options\n if (!member) return noHostPermissions()\n if (isOrgWideMember(member)) {\n const granted = resolveOrgPermissions(member, customRole ?? null)\n return Object.fromEntries(\n hostPermissionKeys().map((key) => [key, granted[key] === true]),\n )\n }\n return resolveCollaboratorHostPermissions(member, hostId) ?? noHostPermissions()\n}\n\n/**\n * The `memberPermissions` projection stamped on a host document beside\n * `memberRoles`: every member who can reach the site, with the verdict for\n * every per-site key they hold ON it. Org-wide members are resolved against\n * the custom roles the caller loaded (one read per distinct role, as\n * `syncOrgAuthProjections` already does for `resolvedPermissions`);\n * collaborators against their host role. A member who cannot reach the site\n * has no entry, matching `projectHostMemberRoles`.\n */\nexport function projectHostMemberPermissions(\n members: ReadonlyArray<Partial<AglynOrgMember> & { $id: string }>,\n hostId: HostUid,\n customRoles: ReadonlyMap<string, AglynOrgCustomRole | null>,\n): Record<string, Record<HostPermissionKey, boolean>> {\n const projection: Record<string, Record<HostPermissionKey, boolean>> = {}\n for (const member of members) {\n if (!hostRoleFor(member, hostId)) continue\n projection[member.$id] = resolveMemberHostPermissions({\n member,\n customRole: member.roleId ? (customRoles.get(member.roleId) ?? null) : null,\n hostId,\n })\n }\n return projection\n}\n"],"names":["listPluginOrgPermissions","hostRoleFor","isOrgWideMember","resolveOrgPermissions","hostPermissionKeys","filter","permission","hostRoleDefaults","undefined","map","key","hostRolePermissionDefaults","role","defaults","noHostPermissions","Object","fromEntries","resolveCollaboratorHostPermissions","member","hostId","merged","toggles","hostPermissions","keys","value","resolveMemberHostPermissions","options","customRole","granted","projectHostMemberPermissions","members","customRoles","projection","$id","roleId","get"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAQD,SAASA,wBAAwB,QAAQ,2CAAuC;AAChF,SAASC,WAAW,EAAEC,eAAe,QAAQ,qBAAiB;AAC9D,SAASC,qBAAqB,QAAiC,uBAAmB;AAElF;;;;;;;;;;;;;;;;;;;CAmBC,GAED,yEAAyE,GACzE,OAAO,SAASC;IACd,OAAOJ,2BACJK,MAAM,CAAC,CAACC,aAAeA,WAAWC,gBAAgB,KAAKC,WACvDC,GAAG,CAAC,CAACH,aAAeA,WAAWI,GAAG;AACvC;AAEA,uEAAuE,GACvE,OAAO,SAASC,2BACdC,IAAoB;IAEpB,MAAMC,WAA+C,CAAC;IACtD,KAAK,MAAMP,cAAcN,2BAA4B;QACnD,IAAI,CAACM,WAAWC,gBAAgB,EAAE;QAClCM,QAAQ,CAACP,WAAWI,GAAG,CAAC,GAAGJ,WAAWC,gBAAgB,CAACK,KAAK,KAAK;IACnE;IACA,OAAOC;AACT;AAEA,iCAAiC,GACjC,SAASC;IACP,OAAOC,OAAOC,WAAW,CAACZ,qBAAqBK,GAAG,CAAC,CAACC,MAAQ;YAACA;YAAK;SAAM;AAC1E;AAEA;;;;;CAKC,GACD,OAAO,SAASO,mCACdC,MAAkD,EAClDC,MAAkC;QAMlBD;IAJhB,IAAI,CAACA,UAAU,CAACC,QAAQ,OAAO;IAC/B,MAAMP,OAAOX,YAAYiB,QAAQC;IACjC,IAAI,CAACP,MAAM,OAAO;IAClB,MAAMQ,SAAST,2BAA2BC;IAC1C,MAAMS,WAAUH,0BAAAA,OAAOI,eAAe,qBAAtBJ,uBAAwB,CAACC,OAAO;IAChD,IAAIE,SAAS;QACX,KAAK,MAAMX,OAAOK,OAAOQ,IAAI,CAACH,QAAS;YACrC,MAAMI,QAAQH,OAAO,CAACX,IAAI;YAC1B,IAAI,OAAOc,UAAU,WAAWJ,MAAM,CAACV,IAAI,GAAGc;QAChD;IACF;IACA,OAAOJ;AACT;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASK,6BAA6BC,OAI5C;QASQT;IARP,MAAM,EAAEC,MAAM,EAAES,UAAU,EAAER,MAAM,EAAE,GAAGO;IACvC,IAAI,CAACR,QAAQ,OAAOJ;IACpB,IAAIZ,gBAAgBgB,SAAS;QAC3B,MAAMU,UAAUzB,sBAAsBe,QAAQS,qBAAAA,aAAc;QAC5D,OAAOZ,OAAOC,WAAW,CACvBZ,qBAAqBK,GAAG,CAAC,CAACC,MAAQ;gBAACA;gBAAKkB,OAAO,CAAClB,IAAI,KAAK;aAAK;IAElE;IACA,QAAOO,sCAAAA,mCAAmCC,QAAQC,mBAA3CF,sCAAsDH;AAC/D;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASe,6BACdC,OAAiE,EACjEX,MAAe,EACfY,WAA2D;IAE3D,MAAMC,aAAiE,CAAC;IACxE,KAAK,MAAMd,UAAUY,QAAS;YAIGC;QAH/B,IAAI,CAAC9B,YAAYiB,QAAQC,SAAS;QAClCa,UAAU,CAACd,OAAOe,GAAG,CAAC,GAAGR,6BAA6B;YACpDP;YACAS,YAAYT,OAAOgB,MAAM,IAAIH,mBAAAA,YAAYI,GAAG,CAACjB,OAAOgB,MAAM,aAA7BH,mBAAkC,OAAQ;YACvEZ;QACF;IACF;IACA,OAAOa;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-permissions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n AglynOrgMember,\n HostAccessRole,\n HostPermissionKey,\n HostUid,\n OrgRole,\n} from '../foundation'\nimport { refinePermissionMap, type PermissionRefinement } from '../plugin-manager/plugin-permissions'\nimport { hostRoleFor, isOrgWideMember } from './organizations'\nimport { CORE_SITE_ROLE_PRESETS } from './org-role-grants'\nimport {\n permissionRefusalText,\n ORG_PERMISSION_REFINEMENTS,\n ORG_PERMISSIONS,\n resolveOrgPermissions,\n type AglynOrgCustomRole,\n type OrgPermissionDefinition,\n} from './org-permissions'\nimport { orgRoleTier } from './org-roles'\n\n/**\n * Catalog keys a site collaborator holds PER SITE (AGL-2984), resolved on one\n * of two axes.\n *\n * A plugin that declares a catalog key with `hostRoleDefaults` makes it a\n * per-site key. For the people whose membership spans the workspace, the key\n * is an ordinary catalog key: their org role's default, refined by a custom\n * role and by per-member overrides, the same on every site. A site\n * collaborator has no org standing to refine — their membership exists only\n * to carry access to named sites — so for them the key is decided PER SITE,\n * from the host role they hold there, refined by `hostPermissions[hostId]` on\n * their member document.\n *\n * Which axis applies is `isOrgWideMember`, the predicate the rules projection\n * and the scope resolver already share. A collaborator asked about a site\n * they were never granted resolves to nothing, and so does one whose request\n * names no site at all: there is no host role to read a default from, and\n * guessing one would let a request reach around the per-site toggle by\n * omitting the site.\n */\n\n/**\n * The catalog keys a collaborator can hold per site, in catalog order: every\n * key with host-role defaults, the core's site keys (AGL-4047) and the ones\n * plugins declare.\n */\nexport function hostPermissionKeys(): HostPermissionKey[] {\n return ORG_PERMISSIONS.filter((definition) => definition.hostRoleDefaults !== undefined).map(\n (definition) => definition.key,\n )\n}\n\n/**\n * What a host role grants a site collaborator for every per-site key, before\n * any toggle. A key whose site-role preset holds for members of the whole\n * workspace only (`workspaceMembersOnly`, AGL-4047) grants a collaborator\n * nothing, whatever their site role.\n */\nexport function hostRolePermissionDefaults(\n role: HostAccessRole,\n): Record<HostPermissionKey, boolean> {\n const defaults: Record<HostPermissionKey, boolean> = {}\n for (const definition of ORG_PERMISSIONS) {\n if (!definition.hostRoleDefaults) continue\n defaults[definition.key] = !definition.workspaceMembersOnly && definition.hostRoleDefaults[role] === true\n }\n return defaults\n}\n\n/** Every per-site key, refused. */\nfunction noHostPermissions(): Record<HostPermissionKey, boolean> {\n return Object.fromEntries(hostPermissionKeys().map((key) => [key, false]))\n}\n\n/**\n * The catalog's refinements whose key and broader key are both in `map`\n * (AGL-4047): the links a per-site map can settle on its own. A link to a\n * key the map does not carry would read that key as refused and add it.\n */\nfunction refinementsWithin(map: Readonly<Record<string, boolean>>): PermissionRefinement[] {\n return ORG_PERMISSION_REFINEMENTS.filter(([key, parent]) => key in map && parent in map)\n}\n\n/**\n * A collaborator's verdict on ONE site for every per-site key, or null where\n * they have no access to that site at all. An absent toggle reads as the host\n * role's default, which is what makes a member document written before\n * `hostPermissions` existed resolve exactly as it did before.\n */\nexport function resolveCollaboratorHostPermissions(\n member: Partial<AglynOrgMember> | null | undefined,\n hostId: HostUid | null | undefined,\n): Record<HostPermissionKey, boolean> | null {\n if (!member || !hostId) return null\n const role = hostRoleFor(member, hostId)\n if (!role) return null\n const merged = hostRolePermissionDefaults(role)\n const toggles = member.hostPermissions?.[hostId]\n if (toggles) {\n for (const key of Object.keys(merged)) {\n const value = toggles[key]\n if (typeof value === 'boolean') merged[key] = value\n }\n }\n // A per-site key that follows or refines another per-site key (AGL-4047)\n // takes its verdict from that key on this site where the site sets nothing\n // for it, as the org catalog's resolver does with a custom role.\n return refinePermissionMap(merged, refinementsWithin(merged), [toggles])\n}\n\n/*\n * KEYS WHOSE PRESET IS THE SITE ROLE (AGL-4047).\n *\n * Some checks were written against the member's role on a site: who may book\n * a walk-in, change a stay or edit a site's reservations asked whether their\n * `memberRoles` entry was Admin, Editor or Author. Roles are only presets of\n * permissions, so those checks became keys, and a key that replaces a gate\n * on the site role keeps that role as its preset (`presetFromSiteRole`):\n *\n * 1. the member's role ON THE SITE (`hostRoleFor`): Admin for the owner and\n * admins, a workspace-wide editor's or viewer's own role unless the site\n * gives them other access there, narrower or wider, and a collaborator's\n * site role; no access to the site holds nothing;\n * 2. that role's `hostRoleDefaults` verdict for the key;\n * 3. then, for a workspace-wide member, their custom role's verdict and\n * their own permissions' verdict, when either sets the key;\n * 4. then the site's own toggle for them (`hostPermissions[hostId][key]`).\n *\n * With nothing set at 3 or 4, the verdict is exactly what the role gate\n * answered, for every member on every site, which is how the built-in roles\n * keep the access they had. A collaborator has no custom role and no\n * workspace standing to refine, so only the site's toggle moves their\n * verdict, as for every other per-site key.\n *\n * A key declared `workspaceMembersOnly` replaces a gate that asked for an\n * admin of the whole workspace who is also this site's admin: its preset at\n * 2 is the site role's for a member of the whole workspace and refusal for a\n * collaborator, whose site role (even Admin) never carried it. The site's\n * toggle at 4 still grants it to them.\n *\n * These keys narrow nothing (`narrowsPerSite` is dropped): the site role is\n * already their preset. With no site named, the org catalog's verdict\n * (`roleDefaults`, then the layers) stands, which is the same answer for a\n * workspace-wide member on a site where their access is their workspace role.\n */\n\n/** The catalog keys whose preset is the member's role on the site, in catalog order. */\nexport function siteRolePresetPermissionKeys(): string[] {\n return ORG_PERMISSIONS.filter((definition) => definition.presetFromSiteRole).map(\n (definition) => definition.key,\n )\n}\n\n/** Is this a key whose preset is the member's role on the site? */\nexport function isSiteRolePresetPermission(permission: string): boolean {\n return ORG_PERMISSIONS.some(\n (definition) => definition.key === permission && definition.presetFromSiteRole === true,\n )\n}\n\n/**\n * A member's verdict on ONE site for every key whose preset is their role\n * there (see above). `customRole` is their custom role document when they\n * carry a `roleId` and the caller has it, and is ignored for a collaborator.\n * No site named, or no access to it, refuses every such key.\n */\nexport function resolveSiteRolePresetPermissions(\n member: (Partial<AglynOrgMember> & { roleId?: string | null }) | null | undefined,\n hostId: HostUid | null | undefined,\n customRole?: Pick<AglynOrgCustomRole, 'permissions'> | null,\n): Record<string, boolean> {\n const definitions = ORG_PERMISSIONS.filter((definition) => definition.presetFromSiteRole)\n if (!definitions.length) return {}\n const role = member && hostId ? hostRoleFor(member, hostId) : null\n if (!member || !hostId || !role) {\n return Object.fromEntries(definitions.map((definition) => [definition.key, false]))\n }\n const toggles = member.hostPermissions?.[hostId]\n const workspaceMember = isOrgWideMember(member)\n const layers: ReadonlyArray<Readonly<Partial<Record<string, unknown>>> | null | undefined> =\n workspaceMember\n ? [member.roleId ? customRole?.permissions : undefined, member.permissions, toggles]\n : [toggles]\n const out: Record<string, boolean> = {}\n for (const definition of definitions) {\n let verdict =\n (workspaceMember || !definition.workspaceMembersOnly) && definition.hostRoleDefaults?.[role] === true\n for (const layer of layers) {\n const value = layer?.[definition.key]\n if (typeof value === 'boolean') verdict = value\n }\n out[definition.key] = verdict\n }\n return refinePermissionMap(out, refinementsWithin(out), layers)\n}\n\n/**\n * `granted` with every key whose preset is the site role decided for this\n * site. With no site named it is `granted` as given, so a resolver can call\n * it unconditionally.\n */\nexport function applySiteRolePresets<T extends Record<string, boolean>>(\n granted: T,\n member: (Partial<AglynOrgMember> & { roleId?: string | null }) | null | undefined,\n hostId: HostUid | null | undefined,\n customRole?: Pick<AglynOrgCustomRole, 'permissions'> | null,\n): T {\n if (!hostId) return granted\n return { ...granted, ...resolveSiteRolePresetPermissions(member, hostId, customRole) }\n}\n\n/**\n * The verdict the site's own stamp gives a member for one key whose preset\n * is the site role, as the Firestore rules read it (AGL-4047): the site's\n * `memberPermissions[uid][key]` when the projection wrote one, else the\n * preset of the role the site's `memberRoles` projection holds for them.\n *\n * For a door that has the host document in hand and asks what the rules\n * ask, so the two cannot disagree: the stamp is written beside\n * `memberRoles` by the same projection, with every layer applied, and a\n * site the projection has not reached since the key existed answers exactly\n * as the role gate it replaced. `presetFor` is the declaring plugin's own\n * table, so the answer does not depend on which plugins this process loaded.\n */\nexport function hostStampedSitePermission(\n host: { memberPermissions?: unknown; memberRoles?: unknown } | null | undefined,\n uid: string,\n permission: string,\n presetFor: Readonly<Partial<Record<HostAccessRole, boolean>>>,\n): boolean {\n const stamped = (host?.memberPermissions as Record<string, Record<string, unknown> | undefined> | undefined)?.[uid]?.[\n permission\n ]\n if (typeof stamped === 'boolean') return stamped\n const role = (host?.memberRoles as Record<string, unknown> | undefined)?.[uid]\n return typeof role === 'string' && presetFor[role as HostAccessRole] === true\n}\n\n/**\n * The same for a key whose preset holds for members of the whole workspace\n * only (`workspaceMembersOnly`, AGL-4047), which `memberRoles` alone cannot\n * answer: an admin of one site and an admin of the whole workspace project\n * the same `'admin'` there. The site's stamp when the projection wrote one;\n * else the preset of the role `memberRoles` holds for them AND of the role\n * the fresh resolution gives them on the site, for a member of the whole\n * workspace only. Exactly the gate these keys replaced: the projection's\n * role, `orgWide` and the fresh site role, all three.\n */\nexport function hostStampedWorkspaceSitePermission(\n host: { memberPermissions?: unknown; memberRoles?: unknown } | null | undefined,\n uid: string,\n permission: string,\n presetFor: Readonly<Partial<Record<HostAccessRole, boolean>>>,\n membership: { orgWide: boolean; hostRole: string | null | undefined },\n): boolean {\n const stamped = (host?.memberPermissions as Record<string, Record<string, unknown> | undefined> | undefined)?.[uid]?.[\n permission\n ]\n if (typeof stamped === 'boolean') return stamped\n if (membership.orgWide !== true) return false\n const fresh = membership.hostRole\n return (\n typeof fresh === 'string' &&\n presetFor[fresh as HostAccessRole] === true &&\n hostStampedSitePermission({ memberRoles: host?.memberRoles }, uid, permission, presetFor)\n )\n}\n\n/*\n * A SITE NARROWS A WORKSPACE-WIDE MEMBER (AGL-4047).\n *\n * An editor who works across the workspace can be given Viewer or Author\n * access to one site (`hostAccess[hostId]`, which `hostRoleFor` reads ahead\n * of `allHosts`). That narrowed their content rights there and nothing else,\n * so a key the workspace grants an editor, such as working a site's\n * register, still held on the site they were narrowed on.\n *\n * A plugin marks the keys a site CAN narrow with `narrowsPerSite`, and\n * whether each one does is a control with three layers, the nearest set one\n * winning (`siteNarrowingChoices`):\n *\n * - the member's own choice (`member.siteNarrowing[key]`);\n * - else their custom role's (`customRole.siteNarrowing[key]`);\n * - else the built-in role's default the declaration names.\n *\n * `true` is \"Follow the site role\", `false` is \"Keep this permission\". On a\n * site that narrows the member, each key that follows the site role is\n * decided for that site:\n *\n * 1. the site's own verdict for the key (`hostPermissions[hostId][key]`),\n * when the site set one;\n * 2. else, for a key that refines another key that also follows the site\n * role, that key's verdict on this site, never more than the workspace\n * grants;\n * 3. else the narrower access's default for the key, never more than the\n * workspace grants;\n *\n * and a key they keep is the workspace's verdict, as everywhere else. A finer\n * key they hold on the site (one the site grants, or one they keep and the\n * workspace grants) brings the key it refines on this site with it, unless\n * the site refuses that key, because a refining key is never held without\n * it. The other finer keys do not follow that implied grant: allowing \"sell\"\n * on a site is not allowing refunds.\n *\n * A member whose access to every site matches their workspace role (the\n * owner and admins always do), one who keeps every key a site can narrow,\n * and a site collaborator, whose keys come from their site role already, all\n * resolve exactly as before.\n */\n\n/** Host roles from the widest down, for \"is this site's access narrower?\". */\nconst HOST_ROLE_RANK: Readonly<Record<HostAccessRole, number>> = {\n admin: 3,\n editor: 2,\n author: 1,\n viewer: 0,\n}\n\n/** How one site narrows a workspace-wide member: the access they hold there, the site's verdicts, and which keys follow it. */\nexport interface SiteNarrowing {\n /** Their role on the site, narrower than their workspace role. */\n role: HostAccessRole\n /** `hostPermissions[hostId]`: the verdicts the site set for them. */\n grants: Readonly<Partial<Record<string, unknown>>>\n /**\n * Every key a site can narrow, `true` where it follows the site role for\n * this member and `false` where they keep it (`siteNarrowingChoices`).\n */\n narrows: Readonly<Record<string, boolean>>\n}\n\n/** The catalog keys a site CAN narrow (`narrowsPerSite`), in catalog order. */\nexport function siteNarrowedPermissionKeys(): string[] {\n return ORG_PERMISSIONS.filter((definition) => definition.narrowsPerSite).map(\n (definition) => definition.key,\n )\n}\n\n/**\n * Could any site narrow a member of this org role? Not the owner or an\n * admin, who are admins of every site, and not a role whose site access has\n * nothing narrower below it (a viewer). The editors' control is the one that\n * changes anything; the role editor says so beside it.\n */\nexport function roleCanBeNarrowed(role: string | null | undefined): boolean {\n if (role === 'owner' || role === 'admin') return false\n return HOST_ROLE_RANK[orgRoleTier(role) as HostAccessRole] > 0\n}\n\n/**\n * Whether each key a site can narrow follows the site role for this member\n * (`true`) or is kept (`false`): their own choice, else their custom role's\n * (when they carry its id and it was supplied), else the built-in role's\n * default.\n */\nexport function siteNarrowingChoices(\n member: (Partial<AglynOrgMember> & { roleId?: string | null }) | null | undefined,\n customRole?: Pick<AglynOrgCustomRole, 'siteNarrowing'> | null,\n): Record<string, boolean> {\n const role = (member?.role ?? 'viewer') as OrgRole\n const own = member?.siteNarrowing ?? {}\n const fromRole = member?.roleId ? (customRole?.siteNarrowing ?? {}) : {}\n const out: Record<string, boolean> = {}\n for (const definition of ORG_PERMISSIONS) {\n if (!definition.narrowsPerSite) continue\n const key = definition.key\n const mine = own[key]\n const theirRole = fromRole[key]\n out[key] =\n typeof mine === 'boolean'\n ? mine\n : typeof theirRole === 'boolean'\n ? theirRole\n : definition.narrowsPerSite[role] === true\n }\n return out\n}\n\n/**\n * Is this member's access to this site narrower than their workspace role?\n * The cheap half of `siteNarrowingFor`, which needs no custom role: a caller\n * that has to read one asks this first, and reads nothing for everyone else.\n */\nexport function siteMayNarrow(\n member: Partial<AglynOrgMember> | null | undefined,\n hostId: HostUid | null | undefined,\n): HostAccessRole | null {\n if (!member || !hostId || !isOrgWideMember(member)) return null\n const role = hostRoleFor(member, hostId)\n if (!role) return null\n const workspaceRole = orgRoleTier(member.role) as HostAccessRole\n if (HOST_ROLE_RANK[role] >= HOST_ROLE_RANK[workspaceRole]) return null\n return role\n}\n\n/**\n * How this site narrows this member, or null where it does not: a member\n * who is not workspace-wide, has no access to the site, whose access there\n * is as wide as their workspace role (owners and admins always), or who\n * keeps every key a site can narrow. `customRole` is their custom role\n * document when they carry a `roleId`; without it, only their own choices\n * and the built-in defaults decide.\n */\nexport function siteNarrowingFor(\n member: Partial<AglynOrgMember> | null | undefined,\n hostId: HostUid | null | undefined,\n customRole?: Pick<AglynOrgCustomRole, 'siteNarrowing'> | null,\n): SiteNarrowing | null {\n const role = siteMayNarrow(member, hostId)\n if (!role) return null\n const narrows = siteNarrowingChoices(member, customRole)\n if (!Object.values(narrows).some(Boolean)) return null\n return { role, grants: member?.hostPermissions?.[hostId as HostUid] ?? {}, narrows }\n}\n\n/** The keys that follow the site role under a narrowing, in catalog order; none without one. */\nexport function narrowedPermissionKeysOf(narrowing: SiteNarrowing | null | undefined): string[] {\n if (!narrowing) return []\n return siteNarrowedPermissionKeys().filter((key) => narrowing.narrows[key] === true)\n}\n\n/** What a site access holds by default for a key: its site default, else its tier's (Author on the Viewer tier). */\nfunction narrowedDefault(definition: OrgPermissionDefinition, role: HostAccessRole): boolean {\n const site = definition.hostRoleDefaults?.[role]\n if (typeof site === 'boolean') return site\n return definition.roleDefaults[orgRoleTier(role)] === true\n}\n\n/**\n * A workspace-wide member's resolved map with the keys that follow the site\n * role decided for that site (see above). With no narrowing it is the map\n * as given, so every resolver can call it unconditionally.\n */\nexport function narrowPermissionsToSite<T extends Record<string, boolean>>(\n granted: T,\n narrowing: SiteNarrowing | null | undefined,\n): T {\n if (!narrowing) return granted\n const definitions = ORG_PERMISSIONS.filter((definition) => definition.narrowsPerSite)\n if (!definitions.length) return granted\n const narrowable = new Set(definitions.map((definition) => definition.key))\n const follows = (key: string): boolean => narrowing.narrows[key] === true\n // A finer key's broader key, among the keys a site can narrow.\n const broader = new Map<string, string>()\n for (const [key, parent, kind] of ORG_PERMISSION_REFINEMENTS) {\n if (kind !== 'follows' && narrowable.has(key) && narrowable.has(parent)) broader.set(key, parent)\n }\n const siteVerdict = (key: string): boolean | undefined => {\n const value = narrowing.grants[key]\n return typeof value === 'boolean' ? value : undefined\n }\n const depth = (key: string): number => {\n const seen = new Set<string>()\n let steps = 0\n for (let current = key; broader.has(current) && !seen.has(current); steps += 1) {\n seen.add(current)\n current = broader.get(current) as string\n }\n return steps\n }\n const out: Record<string, boolean> = { ...granted }\n for (const definition of [...definitions].sort((a, b) => depth(a.key) - depth(b.key))) {\n const key = definition.key\n const workspace = granted[key] === true\n // Kept: the workspace's verdict, as on every other site.\n if (!follows(key)) {\n out[key] = workspace\n continue\n }\n const own = siteVerdict(key)\n const parent = broader.get(key)\n if (own !== undefined) out[key] = own\n else if (parent && follows(parent)) out[key] = out[parent] === true && workspace\n else out[key] = workspace && narrowedDefault(definition, narrowing.role)\n }\n for (const definition of definitions) {\n const key = definition.key\n const held = follows(key) ? siteVerdict(key) === true : granted[key] === true\n if (!held) continue\n const seen = new Set<string>()\n for (let parent = broader.get(key); parent && !seen.has(parent); parent = broader.get(parent)) {\n seen.add(parent)\n // A kept broader key is the workspace's verdict already.\n if (!follows(parent) || siteVerdict(parent) === false) break\n out[parent] = true\n }\n }\n return refinePermissionMap(out, ORG_PERMISSION_REFINEMENTS) as T\n}\n\n/**\n * A member's verdict for every per-site key, on whichever axis their\n * membership is on.\n *\n * `customRole` is the member's custom role document when they carry a\n * `roleId` and the caller has it in hand, and is ignored for a collaborator,\n * whose org role is a seat-accounting fact rather than a grant. `hostId` is\n * the site the request is about; an org-wide member resolves the same on\n * every site and with none named, except on a site that narrows them.\n *\n * An org-wide member's map also carries every key a site can narrow\n * (AGL-4047), decided for `hostId`, so the site's `memberPermissions`\n * projection answers the rules what the server answers. A collaborator's\n * map does not: their register and waivers keys come from their site role\n * and their own overrides, which the rules read from their member document\n * as before.\n */\nexport function resolveMemberHostPermissions(options: {\n member: Partial<AglynOrgMember> | null | undefined\n customRole?: AglynOrgCustomRole | null\n hostId?: HostUid | null\n}): Record<HostPermissionKey, boolean> {\n const { member, customRole, hostId } = options\n if (!member) return noHostPermissions()\n if (isOrgWideMember(member)) {\n // The keys whose preset is the site role (AGL-4047) are decided for this\n // site last; they narrow nothing, so the narrowing pass leaves them be.\n const granted = applySiteRolePresets(\n narrowPermissionsToSite(\n resolveOrgPermissions(member, customRole ?? null),\n siteNarrowingFor(member, hostId, customRole ?? null),\n ),\n member,\n hostId,\n customRole ?? null,\n )\n return Object.fromEntries(\n [...hostPermissionKeys(), ...siteNarrowedPermissionKeys()].map((key) => [key, granted[key] === true]),\n )\n }\n return resolveCollaboratorHostPermissions(member, hostId) ?? noHostPermissions()\n}\n\n/**\n * The `memberPermissions` projection stamped on a host document beside\n * `memberRoles`: every member who can reach the site, with the verdict for\n * every per-site key they hold ON it. Org-wide members are resolved against\n * the custom roles the caller loaded (one read per distinct role, as\n * `syncOrgAuthProjections` already does for `resolvedPermissions`);\n * collaborators against their host role. A member who cannot reach the site\n * has no entry, matching `projectHostMemberRoles`.\n */\nexport function projectHostMemberPermissions(\n members: ReadonlyArray<Partial<AglynOrgMember> & { $id: string }>,\n hostId: HostUid,\n customRoles: ReadonlyMap<string, AglynOrgCustomRole | null>,\n): Record<string, Record<HostPermissionKey, boolean>> {\n const projection: Record<string, Record<HostPermissionKey, boolean>> = {}\n for (const member of members) {\n if (!hostRoleFor(member, hostId)) continue\n projection[member.$id] = resolveMemberHostPermissions({\n member,\n customRole: member.roleId ? (customRoles.get(member.roleId) ?? null) : null,\n hostId,\n })\n }\n return projection\n}\n\n/** The core's site keys (AGL-4047): `CORE_SITE_ROLE_PRESETS`. */\nexport type CoreSiteKey = 'site.edit' | 'site.publish' | 'site.sensitive' | 'site.manage'\n\n/** A host document, as a snapshot (`get`) or as its data. */\nexport type HostDocumentLike =\n | { get(field: string): unknown }\n | { memberPermissions?: unknown; memberRoles?: unknown }\n | null\n | undefined\n\n/**\n * Does this member hold one of the core's site keys on this site (AGL-4047)?\n * The door's twin of the rules' `canWriteHostContent`,\n * `canPublishHostContent`, `canReadHostSensitive` and site-settings checks:\n * the site's `memberPermissions` stamp, else the preset of the member's\n * `memberRoles` entry (`hostStampedSitePermission`), so a route that asks it\n * answers what the rules answer for the same write. For the routes that read\n * the host document already; it costs no read.\n */\nexport function holdsSiteKey(hostDoc: HostDocumentLike, uid: string, key: CoreSiteKey): boolean {\n return holdsStampedSiteKey(hostDoc, uid, key, CORE_SITE_ROLE_PRESETS[key] ?? {})\n}\n\n/**\n * The same for a plugin's key whose preset is the site role (AGL-4047): the\n * site's `memberPermissions` stamp, else `presetFor` read for the member's\n * `memberRoles` entry. `presetFor` is the declaring plugin's own table, so the\n * answer does not depend on which plugins this process loaded. For the doors\n * and the cards that hold the host document already; it costs no read.\n */\nexport function holdsStampedSiteKey(\n hostDoc: HostDocumentLike,\n uid: string | null | undefined,\n key: string,\n presetFor: Readonly<Partial<Record<HostAccessRole, boolean>>>,\n): boolean {\n if (!hostDoc || !uid) return false\n // `hostDoc`, not `host`: the host-field deny guard reads every `host.…`\n // in this folder as a field of the host document, and `get` is none.\n const read = (field: string): unknown =>\n typeof (hostDoc as { get?: unknown }).get === 'function'\n ? (hostDoc as { get(field: string): unknown }).get(field)\n : (hostDoc as Record<string, unknown>)[field]\n return hostStampedSitePermission(\n { memberPermissions: read('memberPermissions'), memberRoles: read('memberRoles') },\n uid,\n key,\n presetFor,\n )\n}\n\n/**\n * The refusal a door answers when a member does not hold a site key\n * (AGL-4047): it names the permission and who can change it, never a role.\n */\nexport function siteKeyRefusal(key: string): string {\n return permissionRefusalText(key)\n}\n\n"],"names":["refinePermissionMap","hostRoleFor","isOrgWideMember","CORE_SITE_ROLE_PRESETS","permissionRefusalText","ORG_PERMISSION_REFINEMENTS","ORG_PERMISSIONS","resolveOrgPermissions","orgRoleTier","hostPermissionKeys","filter","definition","hostRoleDefaults","undefined","map","key","hostRolePermissionDefaults","role","defaults","workspaceMembersOnly","noHostPermissions","Object","fromEntries","refinementsWithin","parent","resolveCollaboratorHostPermissions","member","hostId","merged","toggles","hostPermissions","keys","value","siteRolePresetPermissionKeys","presetFromSiteRole","isSiteRolePresetPermission","permission","some","resolveSiteRolePresetPermissions","customRole","definitions","length","workspaceMember","layers","roleId","permissions","out","verdict","layer","applySiteRolePresets","granted","hostStampedSitePermission","host","uid","presetFor","stamped","memberPermissions","memberRoles","hostStampedWorkspaceSitePermission","membership","orgWide","fresh","hostRole","HOST_ROLE_RANK","admin","editor","author","viewer","siteNarrowedPermissionKeys","narrowsPerSite","roleCanBeNarrowed","siteNarrowingChoices","own","siteNarrowing","fromRole","mine","theirRole","siteMayNarrow","workspaceRole","siteNarrowingFor","narrows","values","Boolean","grants","narrowedPermissionKeysOf","narrowing","narrowedDefault","site","roleDefaults","narrowPermissionsToSite","narrowable","Set","follows","broader","Map","kind","has","set","siteVerdict","depth","seen","steps","current","add","get","sort","a","b","workspace","held","resolveMemberHostPermissions","options","projectHostMemberPermissions","members","customRoles","projection","$id","holdsSiteKey","hostDoc","holdsStampedSiteKey","read","field","siteKeyRefusal"],"mappings":"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"}
|