@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
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import
|
|
17
|
+
import type { AglynOrgMember } from '../foundation';
|
|
18
|
+
import { type AglynOrgCustomRole, type OrgPermissionDefinition, type OrgPermissionGroup } from './org-permissions';
|
|
18
19
|
/**
|
|
19
20
|
* What a permission editor offers, as logic (AGL-4047): which catalog keys a
|
|
20
21
|
* workspace is shown, filed under which group, narrowed by a search, and the
|
|
@@ -73,3 +74,129 @@ export declare function permissionChoice(stored: Readonly<Partial<Record<string,
|
|
|
73
74
|
export declare function withPermissionChoice(stored: Readonly<Partial<Record<string, boolean>>> | null | undefined, keys: string | readonly string[], choice: PermissionChoice): Record<string, boolean>;
|
|
74
75
|
/** How many keys of a list a stored map grants and denies. */
|
|
75
76
|
export declare function permissionChoiceCounts(stored: Readonly<Partial<Record<string, unknown>>> | null | undefined, keys: readonly string[]): Record<PermissionChoice, number>;
|
|
77
|
+
/**
|
|
78
|
+
* Which layer decided a member's verdict for a key: their org role's default,
|
|
79
|
+
* their custom role, their own override, (`refines`) the broader key a key
|
|
80
|
+
* refines, which it follows where nothing sets it, or (`follows`) the key a
|
|
81
|
+
* key follows without needing it (AGL-4047).
|
|
82
|
+
*/
|
|
83
|
+
export type PermissionSource = 'role' | 'customRole' | 'override' | 'refines' | 'follows';
|
|
84
|
+
/** One key's verdict for a member, and where it came from. */
|
|
85
|
+
export interface ExplainedPermission {
|
|
86
|
+
granted: boolean;
|
|
87
|
+
source: PermissionSource;
|
|
88
|
+
/** The org role's default, what remains when every layer above is cleared. */
|
|
89
|
+
roleDefault: boolean;
|
|
90
|
+
/** The custom role's stored value, when the member carries one that sets the key. */
|
|
91
|
+
customRole?: boolean;
|
|
92
|
+
/** The member's own stored override, when set. */
|
|
93
|
+
override?: boolean;
|
|
94
|
+
/** The broader key this one refines, when it does. */
|
|
95
|
+
refines?: string;
|
|
96
|
+
/** The key this one follows without needing it, when it does. */
|
|
97
|
+
follows?: string;
|
|
98
|
+
/**
|
|
99
|
+
* The broader key that holds this one back: set when a custom role or an
|
|
100
|
+
* override allows the key but the key it refines is not granted.
|
|
101
|
+
*/
|
|
102
|
+
heldBy?: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A member's verdict for every catalog key with the layer that produced it
|
|
106
|
+
* (AGL-4047): the org role's default, then the custom role (when the member
|
|
107
|
+
* carries its id and it was supplied), then the member's own override — the
|
|
108
|
+
* precedence `resolveOrgPermissions` applies, which decides `granted`.
|
|
109
|
+
*/
|
|
110
|
+
export declare function explainOrgPermissions(member: (Partial<AglynOrgMember> & {
|
|
111
|
+
roleId?: string | null;
|
|
112
|
+
permissions?: Partial<Record<string, boolean>>;
|
|
113
|
+
}) | null | undefined, customRole?: AglynOrgCustomRole | null, definitions?: readonly OrgPermissionDefinition[]): Record<string, ExplainedPermission>;
|
|
114
|
+
/** What a custom role or a member stores for one key's narrowing: nothing, follow the site role (`true`), or keep it (`false`). */
|
|
115
|
+
export type NarrowingChoice = 'inherit' | 'narrow' | 'keep';
|
|
116
|
+
export declare const NARROWING_CHOICES: readonly NarrowingChoice[];
|
|
117
|
+
/** The keys of a list a site can narrow, in its order. */
|
|
118
|
+
export declare function narrowableOrgPermissions(definitions?: readonly OrgPermissionDefinition[]): OrgPermissionDefinition[];
|
|
119
|
+
/** The stored narrowing choice for a key; anything but a boolean reads as inherit. */
|
|
120
|
+
export declare function narrowingChoice(stored: Readonly<Partial<Record<string, unknown>>> | null | undefined, key: string): NarrowingChoice;
|
|
121
|
+
/** The stored narrowing map with keys set to a choice: `inherit` deletes the key. */
|
|
122
|
+
export declare function withNarrowingChoice(stored: Readonly<Partial<Record<string, boolean>>> | null | undefined, keys: string | readonly string[], choice: NarrowingChoice): Record<string, boolean>;
|
|
123
|
+
/** Which layer decided whether a key follows a narrower site role for a member. */
|
|
124
|
+
export type NarrowingSource = 'role' | 'customRole' | 'override';
|
|
125
|
+
/** One key's narrowing for a member, and where it came from. */
|
|
126
|
+
export interface ExplainedNarrowing {
|
|
127
|
+
/** It follows the site role where their access is narrower (`false`: they keep it). */
|
|
128
|
+
narrows: boolean;
|
|
129
|
+
source: NarrowingSource;
|
|
130
|
+
/** The built-in role's default, what remains when every layer above is cleared. */
|
|
131
|
+
roleDefault: boolean;
|
|
132
|
+
/** The custom role's stored choice, when the member carries one that sets it. */
|
|
133
|
+
customRole?: boolean;
|
|
134
|
+
/** The member's own stored choice, when set. */
|
|
135
|
+
override?: boolean;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A member's narrowing for every key a site can narrow, with the layer that
|
|
139
|
+
* produced it: their own choice, then their custom role's, then the built-in
|
|
140
|
+
* role's default, the precedence `siteNarrowingChoices` applies.
|
|
141
|
+
*/
|
|
142
|
+
export declare function explainSiteNarrowing(member: (Partial<AglynOrgMember> & {
|
|
143
|
+
roleId?: string | null;
|
|
144
|
+
siteNarrowing?: Partial<Record<string, boolean>>;
|
|
145
|
+
}) | null | undefined, customRole?: Pick<AglynOrgCustomRole, 'siteNarrowing'> | null, definitions?: readonly OrgPermissionDefinition[]): Record<string, ExplainedNarrowing>;
|
|
146
|
+
/**
|
|
147
|
+
* A narrowing write as the grant check reads it: choosing to KEEP a key on
|
|
148
|
+
* narrower sites hands the member that key where they would not otherwise
|
|
149
|
+
* hold it, so it is a grant of the key (`true`); following the site role, or
|
|
150
|
+
* clearing the choice, takes nothing the actor must hold (`false`). Feed it
|
|
151
|
+
* to `overrideWriteRefusal` beside the permission changes.
|
|
152
|
+
*/
|
|
153
|
+
export declare function narrowingChangesAsGrants(changes: PermissionOverrideChanges): PermissionOverrideChanges;
|
|
154
|
+
/** A request's override changes, read: each named key to `true`, `false` or `null` (clear). */
|
|
155
|
+
export type PermissionOverrideChanges = Record<string, boolean | null>;
|
|
156
|
+
/**
|
|
157
|
+
* Reads a body's `permissions` as override changes over the keys a scope
|
|
158
|
+
* may store, or the reason it cannot: a key outside `allowedKeys`, or a
|
|
159
|
+
* value that is not a boolean or null. Nothing is coerced.
|
|
160
|
+
*/
|
|
161
|
+
export declare function parsePermissionOverrides(raw: unknown, allowedKeys: readonly string[]): {
|
|
162
|
+
changes: PermissionOverrideChanges;
|
|
163
|
+
error?: undefined;
|
|
164
|
+
} | {
|
|
165
|
+
changes?: undefined;
|
|
166
|
+
error: string;
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Why a per-member override write is refused, or null when it may go ahead
|
|
170
|
+
* (AGL-4047). The route has already checked the actor may manage members;
|
|
171
|
+
* these are the three things that permission alone does not settle:
|
|
172
|
+
*
|
|
173
|
+
* - the workspace's owner keeps every permission, so no override is stored
|
|
174
|
+
* on them in either direction;
|
|
175
|
+
* - nobody but staff changes their own overrides, which would let a member
|
|
176
|
+
* who manages people widen their own access;
|
|
177
|
+
* - nobody but staff and the owner grants a key they do not hold
|
|
178
|
+
* themselves, so managing people is not a back door to billing or the
|
|
179
|
+
* register.
|
|
180
|
+
*/
|
|
181
|
+
export declare function overrideWriteRefusal(options: {
|
|
182
|
+
targetUid: string;
|
|
183
|
+
targetIsOwner: boolean;
|
|
184
|
+
actorUid: string;
|
|
185
|
+
isStaff: boolean;
|
|
186
|
+
/** The actor owns the workspace, and may grant anything the catalog holds. */
|
|
187
|
+
actorIsOwner?: boolean;
|
|
188
|
+
/** The actor's own resolved verdicts. */
|
|
189
|
+
actorGranted: Readonly<Record<string, boolean>>;
|
|
190
|
+
changes: PermissionOverrideChanges;
|
|
191
|
+
/** Labels for the refusal's words; absent: the keys. */
|
|
192
|
+
labelOf?: (key: string) => string;
|
|
193
|
+
}): {
|
|
194
|
+
status: number;
|
|
195
|
+
error: string;
|
|
196
|
+
} | null;
|
|
197
|
+
/**
|
|
198
|
+
* The override changes that turn `stored` into `draft`: each key whose
|
|
199
|
+
* choice differs, to `true` / `false`, or to `null` where the draft clears
|
|
200
|
+
* it. What an editor sends, so a save writes only what was changed.
|
|
201
|
+
*/
|
|
202
|
+
export declare function overrideChangesBetween(stored: Readonly<Partial<Record<string, unknown>>> | null | undefined, draft: Readonly<Partial<Record<string, unknown>>> | null | undefined): PermissionOverrideChanges;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
* See the License for the specific language governing permissions and
|
|
15
16
|
* limitations under the License.
|
|
16
17
|
*/ import { filterPluginsByReleaseFlags, resolveEnabledPlugins } from "../plugin-manager/enabled-plugins.js";
|
|
17
|
-
import { ORG_PERMISSION_GROUPS, ORG_PERMISSIONS } from "./org-permissions.js";
|
|
18
|
+
import { DEFAULT_ROLE_PERMISSIONS, ORG_PERMISSION_GROUPS, ORG_PERMISSIONS, resolveOrgPermissions } from "./org-permissions.js";
|
|
18
19
|
/**
|
|
19
20
|
* The catalog keys a workspace is offered: every core key, and a plugin's
|
|
20
21
|
* keys while the plugin is on for the workspace and released to it. A key a
|
|
@@ -120,5 +121,179 @@ export const PERMISSION_CHOICES = [
|
|
|
120
121
|
for (const key of keys)counts[permissionChoice(stored, key)] += 1;
|
|
121
122
|
return counts;
|
|
122
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* A member's verdict for every catalog key with the layer that produced it
|
|
126
|
+
* (AGL-4047): the org role's default, then the custom role (when the member
|
|
127
|
+
* carries its id and it was supplied), then the member's own override — the
|
|
128
|
+
* precedence `resolveOrgPermissions` applies, which decides `granted`.
|
|
129
|
+
*/ export function explainOrgPermissions(member, customRole, definitions = ORG_PERMISSIONS) {
|
|
130
|
+
var _ref;
|
|
131
|
+
const role = (_ref = member == null ? void 0 : member.role) != null ? _ref : 'viewer';
|
|
132
|
+
const granted = resolveOrgPermissions(member, customRole != null ? customRole : null);
|
|
133
|
+
const out = {};
|
|
134
|
+
for (const definition of definitions){
|
|
135
|
+
var _DEFAULT_ROLE_PERMISSIONS_role, _customRole_permissions, _member_permissions;
|
|
136
|
+
const key = definition.key;
|
|
137
|
+
const roleDefault = ((_DEFAULT_ROLE_PERMISSIONS_role = DEFAULT_ROLE_PERMISSIONS[role]) == null ? void 0 : _DEFAULT_ROLE_PERMISSIONS_role[key]) === true;
|
|
138
|
+
const fromRole = (member == null ? void 0 : member.roleId) ? customRole == null ? void 0 : (_customRole_permissions = customRole.permissions) == null ? void 0 : _customRole_permissions[key] : undefined;
|
|
139
|
+
const fromOverride = member == null ? void 0 : (_member_permissions = member.permissions) == null ? void 0 : _member_permissions[key];
|
|
140
|
+
const isGranted = member ? granted[key] === true : false;
|
|
141
|
+
const stored = typeof fromOverride === 'boolean' ? fromOverride : typeof fromRole === 'boolean' ? fromRole : undefined;
|
|
142
|
+
out[key] = _extends({
|
|
143
|
+
granted: isGranted,
|
|
144
|
+
source: typeof fromOverride === 'boolean' ? 'override' : typeof fromRole === 'boolean' ? 'customRole' : definition.refines ? 'refines' : definition.follows ? 'follows' : 'role',
|
|
145
|
+
roleDefault
|
|
146
|
+
}, typeof fromRole === 'boolean' ? {
|
|
147
|
+
customRole: fromRole
|
|
148
|
+
} : {}, typeof fromOverride === 'boolean' ? {
|
|
149
|
+
override: fromOverride
|
|
150
|
+
} : {}, definition.refines ? {
|
|
151
|
+
refines: definition.refines
|
|
152
|
+
} : {}, definition.follows ? {
|
|
153
|
+
follows: definition.follows
|
|
154
|
+
} : {}, definition.refines && stored === true && !isGranted ? {
|
|
155
|
+
heldBy: definition.refines
|
|
156
|
+
} : {});
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
export const NARROWING_CHOICES = [
|
|
161
|
+
'inherit',
|
|
162
|
+
'narrow',
|
|
163
|
+
'keep'
|
|
164
|
+
];
|
|
165
|
+
/** The keys of a list a site can narrow, in its order. */ export function narrowableOrgPermissions(definitions = ORG_PERMISSIONS) {
|
|
166
|
+
return definitions.filter((definition)=>Boolean(definition.narrowsPerSite));
|
|
167
|
+
}
|
|
168
|
+
/** The stored narrowing choice for a key; anything but a boolean reads as inherit. */ export function narrowingChoice(stored, key) {
|
|
169
|
+
const value = stored == null ? void 0 : stored[key];
|
|
170
|
+
return value === true ? 'narrow' : value === false ? 'keep' : 'inherit';
|
|
171
|
+
}
|
|
172
|
+
/** The stored narrowing map with keys set to a choice: `inherit` deletes the key. */ export function withNarrowingChoice(stored, keys, choice) {
|
|
173
|
+
return withPermissionChoice(stored, keys, choice === 'inherit' ? 'inherit' : choice === 'narrow' ? 'grant' : 'deny');
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* A member's narrowing for every key a site can narrow, with the layer that
|
|
177
|
+
* produced it: their own choice, then their custom role's, then the built-in
|
|
178
|
+
* role's default, the precedence `siteNarrowingChoices` applies.
|
|
179
|
+
*/ export function explainSiteNarrowing(member, customRole, definitions = ORG_PERMISSIONS) {
|
|
180
|
+
var _ref;
|
|
181
|
+
const role = (_ref = member == null ? void 0 : member.role) != null ? _ref : 'viewer';
|
|
182
|
+
const out = {};
|
|
183
|
+
for (const definition of narrowableOrgPermissions(definitions)){
|
|
184
|
+
var _definition_narrowsPerSite, _customRole_siteNarrowing, _member_siteNarrowing;
|
|
185
|
+
const key = definition.key;
|
|
186
|
+
const roleDefault = ((_definition_narrowsPerSite = definition.narrowsPerSite) == null ? void 0 : _definition_narrowsPerSite[role]) === true;
|
|
187
|
+
const fromRole = (member == null ? void 0 : member.roleId) ? customRole == null ? void 0 : (_customRole_siteNarrowing = customRole.siteNarrowing) == null ? void 0 : _customRole_siteNarrowing[key] : undefined;
|
|
188
|
+
const fromOverride = member == null ? void 0 : (_member_siteNarrowing = member.siteNarrowing) == null ? void 0 : _member_siteNarrowing[key];
|
|
189
|
+
out[key] = _extends({
|
|
190
|
+
narrows: typeof fromOverride === 'boolean' ? fromOverride : typeof fromRole === 'boolean' ? fromRole : roleDefault,
|
|
191
|
+
source: typeof fromOverride === 'boolean' ? 'override' : typeof fromRole === 'boolean' ? 'customRole' : 'role',
|
|
192
|
+
roleDefault
|
|
193
|
+
}, typeof fromRole === 'boolean' ? {
|
|
194
|
+
customRole: fromRole
|
|
195
|
+
} : {}, typeof fromOverride === 'boolean' ? {
|
|
196
|
+
override: fromOverride
|
|
197
|
+
} : {});
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* A narrowing write as the grant check reads it: choosing to KEEP a key on
|
|
203
|
+
* narrower sites hands the member that key where they would not otherwise
|
|
204
|
+
* hold it, so it is a grant of the key (`true`); following the site role, or
|
|
205
|
+
* clearing the choice, takes nothing the actor must hold (`false`). Feed it
|
|
206
|
+
* to `overrideWriteRefusal` beside the permission changes.
|
|
207
|
+
*/ export function narrowingChangesAsGrants(changes) {
|
|
208
|
+
return Object.fromEntries(Object.entries(changes).map(([key, value])=>[
|
|
209
|
+
key,
|
|
210
|
+
value === false
|
|
211
|
+
]));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Reads a body's `permissions` as override changes over the keys a scope
|
|
215
|
+
* may store, or the reason it cannot: a key outside `allowedKeys`, or a
|
|
216
|
+
* value that is not a boolean or null. Nothing is coerced.
|
|
217
|
+
*/ export function parsePermissionOverrides(raw, allowedKeys) {
|
|
218
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
219
|
+
return {
|
|
220
|
+
error: 'permissions must map permission keys to true, false or null'
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const allowed = new Set(allowedKeys);
|
|
224
|
+
const changes = {};
|
|
225
|
+
for (const [key, value] of Object.entries(raw)){
|
|
226
|
+
if (!allowed.has(key)) return {
|
|
227
|
+
error: `Unknown permission "${key}"`
|
|
228
|
+
};
|
|
229
|
+
// Two branches, not one `=== null` union: the core compiles without
|
|
230
|
+
// strictNullChecks, where that comparison does not narrow `unknown`.
|
|
231
|
+
if (typeof value === 'boolean') changes[key] = value;
|
|
232
|
+
else if (value === null) changes[key] = null;
|
|
233
|
+
else return {
|
|
234
|
+
error: `"${key}" must be true, false or null`
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (!Object.keys(changes).length) return {
|
|
238
|
+
error: 'No permission to change'
|
|
239
|
+
};
|
|
240
|
+
return {
|
|
241
|
+
changes
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Why a per-member override write is refused, or null when it may go ahead
|
|
246
|
+
* (AGL-4047). The route has already checked the actor may manage members;
|
|
247
|
+
* these are the three things that permission alone does not settle:
|
|
248
|
+
*
|
|
249
|
+
* - the workspace's owner keeps every permission, so no override is stored
|
|
250
|
+
* on them in either direction;
|
|
251
|
+
* - nobody but staff changes their own overrides, which would let a member
|
|
252
|
+
* who manages people widen their own access;
|
|
253
|
+
* - nobody but staff and the owner grants a key they do not hold
|
|
254
|
+
* themselves, so managing people is not a back door to billing or the
|
|
255
|
+
* register.
|
|
256
|
+
*/ export function overrideWriteRefusal(options) {
|
|
257
|
+
if (options.targetIsOwner) {
|
|
258
|
+
return {
|
|
259
|
+
status: 400,
|
|
260
|
+
error: 'The owner keeps every permission'
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (options.isStaff) return null;
|
|
264
|
+
if (options.targetUid === options.actorUid) {
|
|
265
|
+
return {
|
|
266
|
+
status: 403,
|
|
267
|
+
error: 'Ask another admin to change your own permissions'
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (options.actorIsOwner) return null;
|
|
271
|
+
const beyond = Object.entries(options.changes).filter(([key, value])=>value === true && options.actorGranted[key] !== true).map(([key])=>options.labelOf ? options.labelOf(key) : key);
|
|
272
|
+
if (beyond.length) {
|
|
273
|
+
return {
|
|
274
|
+
status: 403,
|
|
275
|
+
error: `You can only grant permissions you hold yourself: ${beyond.join(', ')}`
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The override changes that turn `stored` into `draft`: each key whose
|
|
282
|
+
* choice differs, to `true` / `false`, or to `null` where the draft clears
|
|
283
|
+
* it. What an editor sends, so a save writes only what was changed.
|
|
284
|
+
*/ export function overrideChangesBetween(stored, draft) {
|
|
285
|
+
const changes = {};
|
|
286
|
+
const keys = new Set([
|
|
287
|
+
...Object.keys(stored != null ? stored : {}),
|
|
288
|
+
...Object.keys(draft != null ? draft : {})
|
|
289
|
+
]);
|
|
290
|
+
for (const key of keys){
|
|
291
|
+
const before = permissionChoice(stored, key);
|
|
292
|
+
const after = permissionChoice(draft, key);
|
|
293
|
+
if (before === after) continue;
|
|
294
|
+
changes[key] = after === 'inherit' ? null : after === 'grant';
|
|
295
|
+
}
|
|
296
|
+
return changes;
|
|
297
|
+
}
|
|
123
298
|
|
|
124
299
|
//# sourceMappingURL=org-permission-editor.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/org-permission-editor.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 { filterPluginsByReleaseFlags, resolveEnabledPlugins } from '../plugin-manager/enabled-plugins'\nimport {\n ORG_PERMISSION_GROUPS,\n ORG_PERMISSIONS,\n type OrgPermissionDefinition,\n type OrgPermissionGroup,\n} from './org-permissions'\n\n/**\n * What a permission editor offers, as logic (AGL-4047): which catalog keys a\n * workspace is shown, filed under which group, narrowed by a search, and the\n * three states a custom role or a per-member override stores for each.\n * Every editor (the custom role dialog, a member's Permissions section, the\n * roster's quick view) reads these, so they cannot offer different keys.\n */\n\n/** Whose keys are offered: the workspace's plugins, after their release flags. */\nexport interface OfferedPermissionsSubject {\n /** The workspace document; its `enabledPlugins` decides which plugins' keys show. */\n org?: { enabledPlugins?: string[] } | null\n /** A release flag's verdict for the workspace; absent: no plugin is held back by a flag. */\n isFlagOn?: (flagKey: string) => boolean\n /** Staff preview: every enabled plugin's keys, flags or not. */\n staffBypass?: boolean\n}\n\n/**\n * The catalog keys a workspace is offered: every core key, and a plugin's\n * keys while the plugin is on for the workspace and released to it. A key a\n * stored role or override already carries for a plugin that is now off is\n * not offered and not dropped: the editors write back the stored map.\n */\nexport function offeredOrgPermissions(\n subject: OfferedPermissionsSubject = {},\n definitions: readonly OrgPermissionDefinition[] = ORG_PERMISSIONS,\n): OrgPermissionDefinition[] {\n const enabled = resolveEnabledPlugins(subject.org ?? null)\n const on = new Set(\n subject.isFlagOn\n ? filterPluginsByReleaseFlags(enabled, subject.isFlagOn, { staffBypass: subject.staffBypass })\n : enabled,\n )\n return definitions.filter((definition) => !definition.pluginId || on.has(definition.pluginId))\n}\n\n/** One group of an editor's list: its heading and its keys, in catalog order. */\nexport interface OrgPermissionSection {\n group: OrgPermissionGroup\n permissions: OrgPermissionDefinition[]\n}\n\n/**\n * The keys filed under their groups, in the groups' display order; a group\n * with no key among `definitions` is left out, and a key whose group the\n * list does not name is filed under a group of its own id.\n */\nexport function groupOrgPermissions(\n definitions: readonly OrgPermissionDefinition[],\n groups: readonly OrgPermissionGroup[] = ORG_PERMISSION_GROUPS,\n): OrgPermissionSection[] {\n const known = new Map(groups.map((group) => [group.id, group]))\n const sections = new Map<string, OrgPermissionSection>()\n for (const group of groups) sections.set(group.id, { group, permissions: [] })\n for (const definition of definitions) {\n let section = sections.get(definition.group)\n if (!section) {\n section = {\n group: known.get(definition.group) ?? { id: definition.group, label: definition.group },\n permissions: [],\n }\n sections.set(definition.group, section)\n }\n section.permissions.push(definition)\n }\n return [...sections.values()].filter((section) => section.permissions.length > 0)\n}\n\n/**\n * The keys a search keeps: every word of the query found, ignoring case, in\n * the key's label, description, key or group heading. An empty query keeps\n * everything.\n */\nexport function searchOrgPermissions(\n definitions: readonly OrgPermissionDefinition[],\n query: string,\n groups: readonly OrgPermissionGroup[] = ORG_PERMISSION_GROUPS,\n): OrgPermissionDefinition[] {\n const words = query.toLowerCase().split(/\\s+/).filter(Boolean)\n if (!words.length) return [...definitions]\n const headings = new Map(groups.map((group) => [group.id, group.label]))\n return definitions.filter((definition) => {\n const haystack = [\n definition.label,\n definition.description,\n definition.key,\n headings.get(definition.group) ?? definition.group,\n ]\n .join(' ')\n .toLowerCase()\n return words.every((word) => haystack.includes(word))\n })\n}\n\n/**\n * What a custom role or a per-member override stores for one key: nothing\n * (`inherit`, the layer below decides), `grant` (true) or `deny` (false).\n */\nexport type PermissionChoice = 'inherit' | 'grant' | 'deny'\n\nexport const PERMISSION_CHOICES: readonly PermissionChoice[] = ['inherit', 'grant', 'deny']\n\n/** The stored choice for a key; anything but a boolean reads as inherit. */\nexport function permissionChoice(\n stored: Readonly<Partial<Record<string, unknown>>> | null | undefined,\n key: string,\n): PermissionChoice {\n const value = stored?.[key]\n return value === true ? 'grant' : value === false ? 'deny' : 'inherit'\n}\n\n/**\n * The stored map with one or more keys set to a choice: `inherit` deletes\n * the key, so the document stores only real decisions. Keys not named are\n * kept as they were, whether or not the editor offers them.\n */\nexport function withPermissionChoice(\n stored: Readonly<Partial<Record<string, boolean>>> | null | undefined,\n keys: string | readonly string[],\n choice: PermissionChoice,\n): Record<string, boolean> {\n const next: Record<string, boolean> = {}\n for (const [key, value] of Object.entries(stored ?? {})) {\n if (typeof value === 'boolean') next[key] = value\n }\n for (const key of typeof keys === 'string' ? [keys] : keys) {\n if (choice === 'inherit') delete next[key]\n else next[key] = choice === 'grant'\n }\n return next\n}\n\n/** How many keys of a list a stored map grants and denies. */\nexport function permissionChoiceCounts(\n stored: Readonly<Partial<Record<string, unknown>>> | null | undefined,\n keys: readonly string[],\n): Record<PermissionChoice, number> {\n const counts: Record<PermissionChoice, number> = { inherit: 0, grant: 0, deny: 0 }\n for (const key of keys) counts[permissionChoice(stored, key)] += 1\n return counts\n}\n"],"names":["filterPluginsByReleaseFlags","resolveEnabledPlugins","ORG_PERMISSION_GROUPS","ORG_PERMISSIONS","offeredOrgPermissions","subject","definitions","enabled","org","on","Set","isFlagOn","staffBypass","filter","definition","pluginId","has","groupOrgPermissions","groups","known","Map","map","group","id","sections","set","permissions","section","get","label","push","values","length","searchOrgPermissions","query","words","toLowerCase","split","Boolean","headings","haystack","description","key","join","every","word","includes","PERMISSION_CHOICES","permissionChoice","stored","value","withPermissionChoice","keys","choice","next","Object","entries","permissionChoiceCounts","counts","inherit","grant","deny"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/org-permission-editor.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 { filterPluginsByReleaseFlags, resolveEnabledPlugins } from '../plugin-manager/enabled-plugins'\nimport type { AglynOrgMember, OrgRole } from '../foundation'\nimport {\n DEFAULT_ROLE_PERMISSIONS,\n ORG_PERMISSION_GROUPS,\n ORG_PERMISSIONS,\n resolveOrgPermissions,\n type AglynOrgCustomRole,\n type OrgPermissionDefinition,\n type OrgPermissionGroup,\n} from './org-permissions'\n\n/**\n * What a permission editor offers, as logic (AGL-4047): which catalog keys a\n * workspace is shown, filed under which group, narrowed by a search, and the\n * three states a custom role or a per-member override stores for each.\n * Every editor (the custom role dialog, a member's Permissions section, the\n * roster's quick view) reads these, so they cannot offer different keys.\n */\n\n/** Whose keys are offered: the workspace's plugins, after their release flags. */\nexport interface OfferedPermissionsSubject {\n /** The workspace document; its `enabledPlugins` decides which plugins' keys show. */\n org?: { enabledPlugins?: string[] } | null\n /** A release flag's verdict for the workspace; absent: no plugin is held back by a flag. */\n isFlagOn?: (flagKey: string) => boolean\n /** Staff preview: every enabled plugin's keys, flags or not. */\n staffBypass?: boolean\n}\n\n/**\n * The catalog keys a workspace is offered: every core key, and a plugin's\n * keys while the plugin is on for the workspace and released to it. A key a\n * stored role or override already carries for a plugin that is now off is\n * not offered and not dropped: the editors write back the stored map.\n */\nexport function offeredOrgPermissions(\n subject: OfferedPermissionsSubject = {},\n definitions: readonly OrgPermissionDefinition[] = ORG_PERMISSIONS,\n): OrgPermissionDefinition[] {\n const enabled = resolveEnabledPlugins(subject.org ?? null)\n const on = new Set(\n subject.isFlagOn\n ? filterPluginsByReleaseFlags(enabled, subject.isFlagOn, { staffBypass: subject.staffBypass })\n : enabled,\n )\n return definitions.filter((definition) => !definition.pluginId || on.has(definition.pluginId))\n}\n\n/** One group of an editor's list: its heading and its keys, in catalog order. */\nexport interface OrgPermissionSection {\n group: OrgPermissionGroup\n permissions: OrgPermissionDefinition[]\n}\n\n/**\n * The keys filed under their groups, in the groups' display order; a group\n * with no key among `definitions` is left out, and a key whose group the\n * list does not name is filed under a group of its own id.\n */\nexport function groupOrgPermissions(\n definitions: readonly OrgPermissionDefinition[],\n groups: readonly OrgPermissionGroup[] = ORG_PERMISSION_GROUPS,\n): OrgPermissionSection[] {\n const known = new Map(groups.map((group) => [group.id, group]))\n const sections = new Map<string, OrgPermissionSection>()\n for (const group of groups) sections.set(group.id, { group, permissions: [] })\n for (const definition of definitions) {\n let section = sections.get(definition.group)\n if (!section) {\n section = {\n group: known.get(definition.group) ?? { id: definition.group, label: definition.group },\n permissions: [],\n }\n sections.set(definition.group, section)\n }\n section.permissions.push(definition)\n }\n return [...sections.values()].filter((section) => section.permissions.length > 0)\n}\n\n/**\n * The keys a search keeps: every word of the query found, ignoring case, in\n * the key's label, description, key or group heading. An empty query keeps\n * everything.\n */\nexport function searchOrgPermissions(\n definitions: readonly OrgPermissionDefinition[],\n query: string,\n groups: readonly OrgPermissionGroup[] = ORG_PERMISSION_GROUPS,\n): OrgPermissionDefinition[] {\n const words = query.toLowerCase().split(/\\s+/).filter(Boolean)\n if (!words.length) return [...definitions]\n const headings = new Map(groups.map((group) => [group.id, group.label]))\n return definitions.filter((definition) => {\n const haystack = [\n definition.label,\n definition.description,\n definition.key,\n headings.get(definition.group) ?? definition.group,\n ]\n .join(' ')\n .toLowerCase()\n return words.every((word) => haystack.includes(word))\n })\n}\n\n/**\n * What a custom role or a per-member override stores for one key: nothing\n * (`inherit`, the layer below decides), `grant` (true) or `deny` (false).\n */\nexport type PermissionChoice = 'inherit' | 'grant' | 'deny'\n\nexport const PERMISSION_CHOICES: readonly PermissionChoice[] = ['inherit', 'grant', 'deny']\n\n/** The stored choice for a key; anything but a boolean reads as inherit. */\nexport function permissionChoice(\n stored: Readonly<Partial<Record<string, unknown>>> | null | undefined,\n key: string,\n): PermissionChoice {\n const value = stored?.[key]\n return value === true ? 'grant' : value === false ? 'deny' : 'inherit'\n}\n\n/**\n * The stored map with one or more keys set to a choice: `inherit` deletes\n * the key, so the document stores only real decisions. Keys not named are\n * kept as they were, whether or not the editor offers them.\n */\nexport function withPermissionChoice(\n stored: Readonly<Partial<Record<string, boolean>>> | null | undefined,\n keys: string | readonly string[],\n choice: PermissionChoice,\n): Record<string, boolean> {\n const next: Record<string, boolean> = {}\n for (const [key, value] of Object.entries(stored ?? {})) {\n if (typeof value === 'boolean') next[key] = value\n }\n for (const key of typeof keys === 'string' ? [keys] : keys) {\n if (choice === 'inherit') delete next[key]\n else next[key] = choice === 'grant'\n }\n return next\n}\n\n/** How many keys of a list a stored map grants and denies. */\nexport function permissionChoiceCounts(\n stored: Readonly<Partial<Record<string, unknown>>> | null | undefined,\n keys: readonly string[],\n): Record<PermissionChoice, number> {\n const counts: Record<PermissionChoice, number> = { inherit: 0, grant: 0, deny: 0 }\n for (const key of keys) counts[permissionChoice(stored, key)] += 1\n return counts\n}\n\n/**\n * Which layer decided a member's verdict for a key: their org role's default,\n * their custom role, their own override, (`refines`) the broader key a key\n * refines, which it follows where nothing sets it, or (`follows`) the key a\n * key follows without needing it (AGL-4047).\n */\nexport type PermissionSource = 'role' | 'customRole' | 'override' | 'refines' | 'follows'\n\n/** One key's verdict for a member, and where it came from. */\nexport interface ExplainedPermission {\n granted: boolean\n source: PermissionSource\n /** The org role's default, what remains when every layer above is cleared. */\n roleDefault: boolean\n /** The custom role's stored value, when the member carries one that sets the key. */\n customRole?: boolean\n /** The member's own stored override, when set. */\n override?: boolean\n /** The broader key this one refines, when it does. */\n refines?: string\n /** The key this one follows without needing it, when it does. */\n follows?: string\n /**\n * The broader key that holds this one back: set when a custom role or an\n * override allows the key but the key it refines is not granted.\n */\n heldBy?: string\n}\n\n/**\n * A member's verdict for every catalog key with the layer that produced it\n * (AGL-4047): the org role's default, then the custom role (when the member\n * carries its id and it was supplied), then the member's own override — the\n * precedence `resolveOrgPermissions` applies, which decides `granted`.\n */\nexport function explainOrgPermissions(\n member: (Partial<AglynOrgMember> & { roleId?: string | null; permissions?: Partial<Record<string, boolean>> }) | null | undefined,\n customRole?: AglynOrgCustomRole | null,\n definitions: readonly OrgPermissionDefinition[] = ORG_PERMISSIONS,\n): Record<string, ExplainedPermission> {\n const role = ((member?.role as OrgRole | undefined) ?? 'viewer') as OrgRole\n const granted = resolveOrgPermissions(member as never, customRole ?? null)\n const out: Record<string, ExplainedPermission> = {}\n for (const definition of definitions) {\n const key = definition.key\n const roleDefault = DEFAULT_ROLE_PERMISSIONS[role]?.[key] === true\n const fromRole = member?.roleId ? customRole?.permissions?.[key] : undefined\n const fromOverride = member?.permissions?.[key]\n const isGranted = member ? granted[key] === true : false\n const stored = typeof fromOverride === 'boolean' ? fromOverride : typeof fromRole === 'boolean' ? fromRole : undefined\n out[key] = {\n granted: isGranted,\n source:\n typeof fromOverride === 'boolean'\n ? 'override'\n : typeof fromRole === 'boolean'\n ? 'customRole'\n : definition.refines\n ? 'refines'\n : definition.follows\n ? 'follows'\n : 'role',\n roleDefault,\n ...(typeof fromRole === 'boolean' ? { customRole: fromRole } : {}),\n ...(typeof fromOverride === 'boolean' ? { override: fromOverride } : {}),\n ...(definition.refines ? { refines: definition.refines } : {}),\n ...(definition.follows ? { follows: definition.follows } : {}),\n ...(definition.refines && stored === true && !isGranted ? { heldBy: definition.refines } : {}),\n }\n }\n return out\n}\n\n/*\n * THE NARROWING CONTROL (AGL-4047). Each key a site can narrow\n * (`narrowsPerSite`) carries a second choice beside allow/deny: on a site\n * where the member's access is narrower than their workspace role, does the\n * key follow that site role, or do they keep it? A custom role and a member\n * store it in `siteNarrowing`, `true` following the site role and `false`\n * keeping the permission, and a key left out inherits.\n */\n\n/** What a custom role or a member stores for one key's narrowing: nothing, follow the site role (`true`), or keep it (`false`). */\nexport type NarrowingChoice = 'inherit' | 'narrow' | 'keep'\n\nexport const NARROWING_CHOICES: readonly NarrowingChoice[] = ['inherit', 'narrow', 'keep']\n\n/** The keys of a list a site can narrow, in its order. */\nexport function narrowableOrgPermissions(\n definitions: readonly OrgPermissionDefinition[] = ORG_PERMISSIONS,\n): OrgPermissionDefinition[] {\n return definitions.filter((definition) => Boolean(definition.narrowsPerSite))\n}\n\n/** The stored narrowing choice for a key; anything but a boolean reads as inherit. */\nexport function narrowingChoice(\n stored: Readonly<Partial<Record<string, unknown>>> | null | undefined,\n key: string,\n): NarrowingChoice {\n const value = stored?.[key]\n return value === true ? 'narrow' : value === false ? 'keep' : 'inherit'\n}\n\n/** The stored narrowing map with keys set to a choice: `inherit` deletes the key. */\nexport function withNarrowingChoice(\n stored: Readonly<Partial<Record<string, boolean>>> | null | undefined,\n keys: string | readonly string[],\n choice: NarrowingChoice,\n): Record<string, boolean> {\n return withPermissionChoice(stored, keys, choice === 'inherit' ? 'inherit' : choice === 'narrow' ? 'grant' : 'deny')\n}\n\n/** Which layer decided whether a key follows a narrower site role for a member. */\nexport type NarrowingSource = 'role' | 'customRole' | 'override'\n\n/** One key's narrowing for a member, and where it came from. */\nexport interface ExplainedNarrowing {\n /** It follows the site role where their access is narrower (`false`: they keep it). */\n narrows: boolean\n source: NarrowingSource\n /** The built-in role's default, what remains when every layer above is cleared. */\n roleDefault: boolean\n /** The custom role's stored choice, when the member carries one that sets it. */\n customRole?: boolean\n /** The member's own stored choice, when set. */\n override?: boolean\n}\n\n/**\n * A member's narrowing for every key a site can narrow, with the layer that\n * produced it: their own choice, then their custom role's, then the built-in\n * role's default, the precedence `siteNarrowingChoices` applies.\n */\nexport function explainSiteNarrowing(\n member:\n | (Partial<AglynOrgMember> & { roleId?: string | null; siteNarrowing?: Partial<Record<string, boolean>> })\n | null\n | undefined,\n customRole?: Pick<AglynOrgCustomRole, 'siteNarrowing'> | null,\n definitions: readonly OrgPermissionDefinition[] = ORG_PERMISSIONS,\n): Record<string, ExplainedNarrowing> {\n const role = ((member?.role as OrgRole | undefined) ?? 'viewer') as OrgRole\n const out: Record<string, ExplainedNarrowing> = {}\n for (const definition of narrowableOrgPermissions(definitions)) {\n const key = definition.key\n const roleDefault = definition.narrowsPerSite?.[role] === true\n const fromRole = member?.roleId ? customRole?.siteNarrowing?.[key] : undefined\n const fromOverride = member?.siteNarrowing?.[key]\n out[key] = {\n narrows:\n typeof fromOverride === 'boolean' ? fromOverride : typeof fromRole === 'boolean' ? fromRole : roleDefault,\n source: typeof fromOverride === 'boolean' ? 'override' : typeof fromRole === 'boolean' ? 'customRole' : 'role',\n roleDefault,\n ...(typeof fromRole === 'boolean' ? { customRole: fromRole } : {}),\n ...(typeof fromOverride === 'boolean' ? { override: fromOverride } : {}),\n }\n }\n return out\n}\n\n/**\n * A narrowing write as the grant check reads it: choosing to KEEP a key on\n * narrower sites hands the member that key where they would not otherwise\n * hold it, so it is a grant of the key (`true`); following the site role, or\n * clearing the choice, takes nothing the actor must hold (`false`). Feed it\n * to `overrideWriteRefusal` beside the permission changes.\n */\nexport function narrowingChangesAsGrants(changes: PermissionOverrideChanges): PermissionOverrideChanges {\n return Object.fromEntries(Object.entries(changes).map(([key, value]) => [key, value === false]))\n}\n\n/** A request's override changes, read: each named key to `true`, `false` or `null` (clear). */\nexport type PermissionOverrideChanges = Record<string, boolean | null>\n\n/**\n * Reads a body's `permissions` as override changes over the keys a scope\n * may store, or the reason it cannot: a key outside `allowedKeys`, or a\n * value that is not a boolean or null. Nothing is coerced.\n */\nexport function parsePermissionOverrides(\n raw: unknown,\n allowedKeys: readonly string[],\n): { changes: PermissionOverrideChanges; error?: undefined } | { changes?: undefined; error: string } {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n return { error: 'permissions must map permission keys to true, false or null' }\n }\n const allowed = new Set(allowedKeys)\n const changes: PermissionOverrideChanges = {}\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (!allowed.has(key)) return { error: `Unknown permission \"${key}\"` }\n // Two branches, not one `=== null` union: the core compiles without\n // strictNullChecks, where that comparison does not narrow `unknown`.\n if (typeof value === 'boolean') changes[key] = value\n else if (value === null) changes[key] = null\n else return { error: `\"${key}\" must be true, false or null` }\n }\n if (!Object.keys(changes).length) return { error: 'No permission to change' }\n return { changes }\n}\n\n/**\n * Why a per-member override write is refused, or null when it may go ahead\n * (AGL-4047). The route has already checked the actor may manage members;\n * these are the three things that permission alone does not settle:\n *\n * - the workspace's owner keeps every permission, so no override is stored\n * on them in either direction;\n * - nobody but staff changes their own overrides, which would let a member\n * who manages people widen their own access;\n * - nobody but staff and the owner grants a key they do not hold\n * themselves, so managing people is not a back door to billing or the\n * register.\n */\nexport function overrideWriteRefusal(options: {\n targetUid: string\n targetIsOwner: boolean\n actorUid: string\n isStaff: boolean\n /** The actor owns the workspace, and may grant anything the catalog holds. */\n actorIsOwner?: boolean\n /** The actor's own resolved verdicts. */\n actorGranted: Readonly<Record<string, boolean>>\n changes: PermissionOverrideChanges\n /** Labels for the refusal's words; absent: the keys. */\n labelOf?: (key: string) => string\n}): { status: number; error: string } | null {\n if (options.targetIsOwner) {\n return { status: 400, error: 'The owner keeps every permission' }\n }\n if (options.isStaff) return null\n if (options.targetUid === options.actorUid) {\n return { status: 403, error: 'Ask another admin to change your own permissions' }\n }\n if (options.actorIsOwner) return null\n const beyond = Object.entries(options.changes)\n .filter(([key, value]) => value === true && options.actorGranted[key] !== true)\n .map(([key]) => (options.labelOf ? options.labelOf(key) : key))\n if (beyond.length) {\n return {\n status: 403,\n error: `You can only grant permissions you hold yourself: ${beyond.join(', ')}`,\n }\n }\n return null\n}\n\n/**\n * The override changes that turn `stored` into `draft`: each key whose\n * choice differs, to `true` / `false`, or to `null` where the draft clears\n * it. What an editor sends, so a save writes only what was changed.\n */\nexport function overrideChangesBetween(\n stored: Readonly<Partial<Record<string, unknown>>> | null | undefined,\n draft: Readonly<Partial<Record<string, unknown>>> | null | undefined,\n): PermissionOverrideChanges {\n const changes: PermissionOverrideChanges = {}\n const keys = new Set([...Object.keys(stored ?? {}), ...Object.keys(draft ?? {})])\n for (const key of keys) {\n const before = permissionChoice(stored, key)\n const after = permissionChoice(draft, key)\n if (before === after) continue\n changes[key] = after === 'inherit' ? null : after === 'grant'\n }\n return changes\n}\n"],"names":["filterPluginsByReleaseFlags","resolveEnabledPlugins","DEFAULT_ROLE_PERMISSIONS","ORG_PERMISSION_GROUPS","ORG_PERMISSIONS","resolveOrgPermissions","offeredOrgPermissions","subject","definitions","enabled","org","on","Set","isFlagOn","staffBypass","filter","definition","pluginId","has","groupOrgPermissions","groups","known","Map","map","group","id","sections","set","permissions","section","get","label","push","values","length","searchOrgPermissions","query","words","toLowerCase","split","Boolean","headings","haystack","description","key","join","every","word","includes","PERMISSION_CHOICES","permissionChoice","stored","value","withPermissionChoice","keys","choice","next","Object","entries","permissionChoiceCounts","counts","inherit","grant","deny","explainOrgPermissions","member","customRole","role","granted","out","roleDefault","fromRole","roleId","undefined","fromOverride","isGranted","source","refines","follows","override","heldBy","NARROWING_CHOICES","narrowableOrgPermissions","narrowsPerSite","narrowingChoice","withNarrowingChoice","explainSiteNarrowing","siteNarrowing","narrows","narrowingChangesAsGrants","changes","fromEntries","parsePermissionOverrides","raw","allowedKeys","Array","isArray","error","allowed","overrideWriteRefusal","options","targetIsOwner","status","isStaff","targetUid","actorUid","actorIsOwner","beyond","actorGranted","labelOf","overrideChangesBetween","draft","before","after"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import type { AglynOrgMember, OrgRole } from '../foundation';
|
|
18
|
+
import { type PermissionRefinement } from '../plugin-manager/plugin-permissions';
|
|
18
19
|
import { type OrgPermissionGroup, type OrgPermissionScope } from './org-role-grants';
|
|
19
20
|
export type { OrgPermissionGroup, OrgPermissionScope };
|
|
20
21
|
/**
|
|
@@ -26,7 +27,7 @@ export type { OrgPermissionGroup, OrgPermissionScope };
|
|
|
26
27
|
* The console hides unpermitted surfaces; the org API routes are the
|
|
27
28
|
* enforcement point.
|
|
28
29
|
*/
|
|
29
|
-
export type CoreOrgPermission = 'org.settings' | 'org.auditLog' | 'billing.view' | 'billing.manage' | 'members.manage' | 'hosts.create' | 'hosts.delete' | 'data.manage' | 'marketplace.publish' | 'plugins.install';
|
|
30
|
+
export type CoreOrgPermission = 'org.settings' | 'org.auditLog' | 'org.apiKeys' | 'org.privacy' | 'org.holds' | 'org.export' | 'org.sendingDomains' | 'org.businessProfile' | 'org.media' | 'org.eraseRecords' | 'billing.view' | 'billing.manage' | 'members.manage' | 'hosts.create' | 'hosts.delete' | 'data.manage' | 'marketplace.publish' | 'plugins.install' | 'site.edit' | 'site.publish' | 'site.sensitive' | 'site.manage';
|
|
30
31
|
/**
|
|
31
32
|
* A key of the catalog: a core key above, or a key a plugin declares into
|
|
32
33
|
* the catalog through `registerPluginEntitlements` (AGL-2984). A declared key
|
|
@@ -62,6 +63,45 @@ export interface OrgPermissionDefinition {
|
|
|
62
63
|
roleDefaults: Readonly<Record<OrgRole, boolean>>;
|
|
63
64
|
/** Each host role's verdict, for a `site` key. */
|
|
64
65
|
hostRoleDefaults?: Readonly<Record<string, boolean>>;
|
|
66
|
+
/**
|
|
67
|
+
* The broader key this one is a finer part of (AGL-4047): never granted
|
|
68
|
+
* without it, and following it where no custom role or override sets this
|
|
69
|
+
* key. Only a plugin's key refines, and only a key the catalog holds.
|
|
70
|
+
*/
|
|
71
|
+
refines?: OrgPermission;
|
|
72
|
+
/**
|
|
73
|
+
* The key this one follows where no custom role or override sets it,
|
|
74
|
+
* without needing it (AGL-4047): granted on its own, it holds while that
|
|
75
|
+
* key is refused. Only a plugin's key follows, and only a key the catalog
|
|
76
|
+
* holds; a key that refines does not also follow.
|
|
77
|
+
*/
|
|
78
|
+
follows?: OrgPermission;
|
|
79
|
+
/**
|
|
80
|
+
* Present when a site can narrow the key (AGL-4047): where a member who
|
|
81
|
+
* works across the workspace has narrower access to one site than their
|
|
82
|
+
* workspace role, whether the key follows that site role there is a
|
|
83
|
+
* control, set per key on a custom role (`siteNarrowing`) and per member
|
|
84
|
+
* (`AglynOrgMember.siteNarrowing`), and this is each built-in role's
|
|
85
|
+
* default for it. `narrowPermissionsToSite` applies it. Only a plugin's key.
|
|
86
|
+
*/
|
|
87
|
+
narrowsPerSite?: Readonly<Record<OrgRole, boolean>>;
|
|
88
|
+
/**
|
|
89
|
+
* The key's preset is the member's role ON THE SITE a request names, for
|
|
90
|
+
* every member (AGL-4047): `hostRoleDefaults` read for the access they hold
|
|
91
|
+
* there, then their custom role, their own permissions and that site's
|
|
92
|
+
* toggle. `roleDefaults` is the answer with no site named.
|
|
93
|
+
* `resolveSiteRolePresetPermissions` decides it. The core's site keys
|
|
94
|
+
* (`CORE_SITE_ROLE_PRESETS`) and a plugin's key with `hostRoleDefaults`.
|
|
95
|
+
*/
|
|
96
|
+
presetFromSiteRole?: true;
|
|
97
|
+
/**
|
|
98
|
+
* With `presetFromSiteRole`: the site role is the preset only for a member
|
|
99
|
+
* of the whole workspace (AGL-4047). A site collaborator's preset is
|
|
100
|
+
* refusal, whatever their site role, and only the site's toggle grants it
|
|
101
|
+
* to them. A plugin's key that replaces a gate asking for an admin of the
|
|
102
|
+
* whole workspace who is also the site's admin.
|
|
103
|
+
*/
|
|
104
|
+
workspaceMembersOnly?: true;
|
|
65
105
|
}
|
|
66
106
|
/**
|
|
67
107
|
* Every permission, in display order for role editors: the core keys, then
|
|
@@ -91,6 +131,13 @@ export declare const ORG_PERMISSION_GROUPS: readonly OrgPermissionGroup[];
|
|
|
91
131
|
* answer so a custom role reaches them. Live.
|
|
92
132
|
*/
|
|
93
133
|
export declare const TIER_PERMISSION_KEYS: readonly OrgPermission[];
|
|
134
|
+
/**
|
|
135
|
+
* Every catalog key that refines a broader one, with that key (AGL-4047),
|
|
136
|
+
* and every key that only follows one, marked `'follows'`. Live, like
|
|
137
|
+
* `ORG_PERMISSIONS`. A resolver lays it over its map with
|
|
138
|
+
* `refinePermissionMap`.
|
|
139
|
+
*/
|
|
140
|
+
export declare const ORG_PERMISSION_REFINEMENTS: readonly PermissionRefinement[];
|
|
94
141
|
/**
|
|
95
142
|
* Role → default permission set. Owner/admin hold everything (owner-only
|
|
96
143
|
* actions like org deletion stay role-checked, not permission-checked);
|
|
@@ -119,6 +166,14 @@ export interface AglynOrgCustomRole {
|
|
|
119
166
|
description?: string;
|
|
120
167
|
/** Full permission map; missing keys read as false. */
|
|
121
168
|
permissions?: Partial<Record<OrgPermission, boolean>>;
|
|
169
|
+
/**
|
|
170
|
+
* Per key a site can narrow (AGL-4047): `true` follows the site role on a
|
|
171
|
+
* site where the member's access is narrower than their workspace role,
|
|
172
|
+
* `false` keeps the permission there. A key it leaves out follows the
|
|
173
|
+
* built-in role's default (`OrgPermissionDefinition.narrowsPerSite`), and
|
|
174
|
+
* a member's own `siteNarrowing` wins over it.
|
|
175
|
+
*/
|
|
176
|
+
siteNarrowing?: Partial<Record<OrgPermission, boolean>>;
|
|
122
177
|
}
|
|
123
178
|
/**
|
|
124
179
|
* Effective permission map for a member: the org role's defaults, then
|
|
@@ -171,6 +226,12 @@ export declare function projectMemberResolvedPermissions(member: Parameters<type
|
|
|
171
226
|
export declare function hasOrgPermission(member: Parameters<typeof resolveOrgPermissions>[0], permission: OrgPermission, customRole?: AglynOrgCustomRole | null): boolean;
|
|
172
227
|
/** The catalog label for a key — the words a refusal names — or the key itself. */
|
|
173
228
|
export declare function orgPermissionLabel(permission: OrgPermission): string;
|
|
229
|
+
/**
|
|
230
|
+
* The sentence a door answers when a member does not hold a key (AGL-4047):
|
|
231
|
+
* it names the permission by its catalog label and who can change it, never
|
|
232
|
+
* a role.
|
|
233
|
+
*/
|
|
234
|
+
export declare function permissionRefusalText(permission: OrgPermission): string;
|
|
174
235
|
/** The keys plugins declared into the catalog, in catalog order. */
|
|
175
236
|
export declare function pluginOrgPermissionKeys(): OrgPermission[];
|
|
176
237
|
/** One declared catalog key that moved, and the direction it moved in. */
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
17
|
import { listPluginCatalogPermissions, subscribePluginEntitlements } from "../plugin-manager/plugin-entitlements.js";
|
|
18
|
-
import { subscribePluginPermissions } from "../plugin-manager/plugin-permissions.js";
|
|
18
|
+
import { permissionRefinementOf, refinePermissionMap, subscribePluginPermissions } from "../plugin-manager/plugin-permissions.js";
|
|
19
19
|
import { ORG_ROLE_PERMISSION_KEYS } from "./org-roles.js";
|
|
20
|
-
import { CORE_ORG_PERMISSION_ENTRIES, CORE_ORG_PERMISSION_GROUPS, CORE_ORG_ROLE_GRANTS } from "./org-role-grants.js";
|
|
20
|
+
import { CORE_ORG_PERMISSION_ENTRIES, CORE_ORG_PERMISSION_GROUPS, CORE_ORG_ROLE_GRANTS, CORE_SITE_ROLE_PRESETS } from "./org-role-grants.js";
|
|
21
21
|
/**
|
|
22
22
|
* The core keys, in display order for role editors.
|
|
23
23
|
*
|
|
@@ -41,11 +41,15 @@ import { CORE_ORG_PERMISSION_ENTRIES, CORE_ORG_PERMISSION_GROUPS, CORE_ORG_ROLE_
|
|
|
41
41
|
* a key here, or a key a plugin declares, gains no server-side consumer.
|
|
42
42
|
*/ const CORE_ORG_PERMISSIONS = // One table, shared with the native apps through the contracts (AGL-3950).
|
|
43
43
|
CORE_ORG_PERMISSION_ENTRIES.map((entry)=>{
|
|
44
|
-
var _entry_group;
|
|
44
|
+
var _entry_group, _entry_scope;
|
|
45
45
|
return _extends({}, entry, {
|
|
46
46
|
key: entry.key,
|
|
47
47
|
group: (_entry_group = entry.group) != null ? _entry_group : 'organization',
|
|
48
|
-
scope: 'org'
|
|
48
|
+
scope: (_entry_scope = entry.scope) != null ? _entry_scope : 'org'
|
|
49
|
+
}, CORE_SITE_ROLE_PRESETS[entry.key] ? {
|
|
50
|
+
hostRoleDefaults: _extends({}, CORE_SITE_ROLE_PRESETS[entry.key]),
|
|
51
|
+
presetFromSiteRole: true
|
|
52
|
+
} : {}, {
|
|
49
53
|
roleDefaults: {
|
|
50
54
|
owner: CORE_ORG_ROLE_GRANTS.owner.includes(entry.key),
|
|
51
55
|
admin: CORE_ORG_ROLE_GRANTS.admin.includes(entry.key),
|
|
@@ -87,6 +91,12 @@ export const ORG_PERMISSION_KEYS = [];
|
|
|
87
91
|
* which a caller holding a catalog-resolved map lays over that resolver's
|
|
88
92
|
* answer so a custom role reaches them. Live.
|
|
89
93
|
*/ export const TIER_PERMISSION_KEYS = [];
|
|
94
|
+
/**
|
|
95
|
+
* Every catalog key that refines a broader one, with that key (AGL-4047),
|
|
96
|
+
* and every key that only follows one, marked `'follows'`. Live, like
|
|
97
|
+
* `ORG_PERMISSIONS`. A resolver lays it over its map with
|
|
98
|
+
* `refinePermissionMap`.
|
|
99
|
+
*/ export const ORG_PERMISSION_REFINEMENTS = [];
|
|
90
100
|
const NO_PERMISSIONS = {};
|
|
91
101
|
/**
|
|
92
102
|
* Role → default permission set. Owner/admin hold everything (owner-only
|
|
@@ -140,6 +150,16 @@ function replaceEntries(target, entries) {
|
|
|
140
150
|
roleDefaults: declared.roleDefaults
|
|
141
151
|
}, declared.hostRoleDefaults ? {
|
|
142
152
|
hostRoleDefaults: declared.hostRoleDefaults
|
|
153
|
+
} : {}, declared.refines ? {
|
|
154
|
+
refines: declared.refines
|
|
155
|
+
} : {}, declared.follows && !declared.refines ? {
|
|
156
|
+
follows: declared.follows
|
|
157
|
+
} : {}, declared.narrowsPerSite ? {
|
|
158
|
+
narrowsPerSite: _extends({}, declared.narrowsPerSite)
|
|
159
|
+
} : {}, declared.presetFromSiteRole && declared.hostRoleDefaults ? {
|
|
160
|
+
presetFromSiteRole: true
|
|
161
|
+
} : {}, declared.presetFromSiteRole && declared.hostRoleDefaults && declared.workspaceMembersOnly ? {
|
|
162
|
+
workspaceMembersOnly: true
|
|
143
163
|
} : {}));
|
|
144
164
|
if (declared.source === 'tier') tierKeys.push(declared.key);
|
|
145
165
|
if (!groups.some((group)=>group.id === declared.group)) {
|
|
@@ -151,6 +171,23 @@ function replaceEntries(target, entries) {
|
|
|
151
171
|
}
|
|
152
172
|
}
|
|
153
173
|
const keys = definitions.map((definition)=>definition.key);
|
|
174
|
+
const known = new Set(keys);
|
|
175
|
+
// A refinement of a key the catalog does not hold refines nothing, and is
|
|
176
|
+
// dropped from the definition so no editor offers a link to nowhere.
|
|
177
|
+
for (const definition of definitions){
|
|
178
|
+
if (definition.refines && (!known.has(definition.refines) || definition.refines === definition.key)) {
|
|
179
|
+
delete definition.refines;
|
|
180
|
+
}
|
|
181
|
+
if (definition.follows && (!known.has(definition.follows) || definition.follows === definition.key)) {
|
|
182
|
+
delete definition.follows;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
replaceItems(ORG_PERMISSION_REFINEMENTS, definitions.flatMap((definition)=>{
|
|
186
|
+
const refinement = permissionRefinementOf(definition);
|
|
187
|
+
return refinement ? [
|
|
188
|
+
refinement
|
|
189
|
+
] : [];
|
|
190
|
+
}));
|
|
154
191
|
replaceItems(ORG_PERMISSIONS, definitions);
|
|
155
192
|
replaceItems(ORG_PERMISSION_KEYS, keys);
|
|
156
193
|
replaceItems(ORG_PERMISSION_GROUPS, groups.filter((group)=>definitions.some((definition)=>definition.group === group.id)));
|
|
@@ -215,7 +252,13 @@ function replaceEntries(target, entries) {
|
|
|
215
252
|
if (typeof value === 'boolean') merged[key] = value;
|
|
216
253
|
}
|
|
217
254
|
}
|
|
218
|
-
|
|
255
|
+
// A key that refines a broader one (AGL-4047) follows it where neither
|
|
256
|
+
// layer set the key, and is never held without it; a key that only follows
|
|
257
|
+
// one takes its verdict where neither layer set the key, and nothing more.
|
|
258
|
+
return refinePermissionMap(merged, ORG_PERMISSION_REFINEMENTS, [
|
|
259
|
+
member.roleId ? customRole == null ? void 0 : customRole.permissions : undefined,
|
|
260
|
+
member.permissions
|
|
261
|
+
]);
|
|
219
262
|
}
|
|
220
263
|
/**
|
|
221
264
|
* The values a custom role and a per-member override set EXPLICITLY for
|
|
@@ -278,6 +321,13 @@ function replaceEntries(target, entries) {
|
|
|
278
321
|
var _ORG_PERMISSIONS_find;
|
|
279
322
|
return (_ref = (_ORG_PERMISSIONS_find = ORG_PERMISSIONS.find((definition)=>definition.key === permission)) == null ? void 0 : _ORG_PERMISSIONS_find.label) != null ? _ref : permission;
|
|
280
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* The sentence a door answers when a member does not hold a key (AGL-4047):
|
|
326
|
+
* it names the permission by its catalog label and who can change it, never
|
|
327
|
+
* a role.
|
|
328
|
+
*/ export function permissionRefusalText(permission) {
|
|
329
|
+
return `Your role does not include "${orgPermissionLabel(permission)}" — ask an organization admin`;
|
|
330
|
+
}
|
|
281
331
|
/** The keys plugins declared into the catalog, in catalog order. */ export function pluginOrgPermissionKeys() {
|
|
282
332
|
return ORG_PERMISSIONS.filter((definition)=>definition.pluginId).map((definition)=>definition.key);
|
|
283
333
|
}
|