@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.
Files changed (91) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-acquisition.d.ts +14 -0
  3. package/src/lib/app-utils/account-acquisition.js +19 -0
  4. package/src/lib/app-utils/account-acquisition.js.map +1 -1
  5. package/src/lib/app-utils/api-plugins.d.ts +25 -0
  6. package/src/lib/app-utils/api-plugins.js +13 -0
  7. package/src/lib/app-utils/api-plugins.js.map +1 -1
  8. package/src/lib/app-utils/business-profile-values.d.ts +15 -3
  9. package/src/lib/app-utils/business-profile-values.js +19 -3
  10. package/src/lib/app-utils/business-profile-values.js.map +1 -1
  11. package/src/lib/app-utils/business-profile.d.ts +5 -3
  12. package/src/lib/app-utils/business-profile.js +2 -1
  13. package/src/lib/app-utils/business-profile.js.map +1 -1
  14. package/src/lib/app-utils/contacts.d.ts +11 -0
  15. package/src/lib/app-utils/contacts.js +18 -0
  16. package/src/lib/app-utils/contacts.js.map +1 -1
  17. package/src/lib/app-utils/docs-deflection.d.ts +1 -1
  18. package/src/lib/app-utils/docs-deflection.js +17 -0
  19. package/src/lib/app-utils/docs-deflection.js.map +1 -1
  20. package/src/lib/app-utils/docs-help-sections.generated.js +5 -2
  21. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  22. package/src/lib/app-utils/docs-help.generated.d.ts +9 -9
  23. package/src/lib/app-utils/docs-help.generated.js +20 -0
  24. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  25. package/src/lib/app-utils/docs-index.generated.js +168 -63
  26. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  27. package/src/lib/app-utils/docs-retrieval.d.ts +17 -0
  28. package/src/lib/app-utils/docs-retrieval.js +28 -0
  29. package/src/lib/app-utils/docs-retrieval.js.map +1 -1
  30. package/src/lib/app-utils/host-naming.d.ts +2 -1
  31. package/src/lib/app-utils/host-naming.js +65 -2
  32. package/src/lib/app-utils/host-naming.js.map +1 -1
  33. package/src/lib/app-utils/host-permissions.d.ts +159 -3
  34. package/src/lib/app-utils/host-permissions.js +384 -12
  35. package/src/lib/app-utils/host-permissions.js.map +1 -1
  36. package/src/lib/app-utils/org-permission-editor.d.ts +128 -1
  37. package/src/lib/app-utils/org-permission-editor.js +176 -1
  38. package/src/lib/app-utils/org-permission-editor.js.map +1 -1
  39. package/src/lib/app-utils/org-permissions.d.ts +62 -1
  40. package/src/lib/app-utils/org-permissions.js +55 -5
  41. package/src/lib/app-utils/org-permissions.js.map +1 -1
  42. package/src/lib/app-utils/org-role-grants.d.ts +28 -0
  43. package/src/lib/app-utils/org-role-grants.js +180 -2
  44. package/src/lib/app-utils/org-role-grants.js.map +1 -1
  45. package/src/lib/app-utils/org-roles.js +11 -2
  46. package/src/lib/app-utils/org-roles.js.map +1 -1
  47. package/src/lib/app-utils/redirect-query.d.ts +39 -0
  48. package/src/lib/app-utils/redirect-query.js +57 -0
  49. package/src/lib/app-utils/redirect-query.js.map +1 -0
  50. package/src/lib/app-utils/release-flags.js +2 -2
  51. package/src/lib/app-utils/release-flags.js.map +1 -1
  52. package/src/lib/app-utils/site-starter-links.d.ts +42 -0
  53. package/src/lib/app-utils/site-starter-links.js +79 -0
  54. package/src/lib/app-utils/site-starter-links.js.map +1 -0
  55. package/src/lib/app-utils/site-starter-nodes.d.ts +91 -0
  56. package/src/lib/app-utils/site-starter-nodes.js +210 -0
  57. package/src/lib/app-utils/site-starter-nodes.js.map +1 -0
  58. package/src/lib/app-utils/site-starter-theme.d.ts +90 -0
  59. package/src/lib/app-utils/site-starter-theme.js +241 -0
  60. package/src/lib/app-utils/site-starter-theme.js.map +1 -0
  61. package/src/lib/app-utils/webhook-delivery.js +6 -1
  62. package/src/lib/app-utils/webhook-delivery.js.map +1 -1
  63. package/src/lib/foundation/definitions/organization.types.d.ts +12 -2
  64. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  65. package/src/lib/plugin-manager/first-party-plugins.generated.js +39 -0
  66. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  67. package/src/lib/plugin-manager/plugin-closure-notices.d.ts +88 -0
  68. package/src/lib/plugin-manager/plugin-closure-notices.js +123 -0
  69. package/src/lib/plugin-manager/plugin-closure-notices.js.map +1 -0
  70. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +15 -0
  71. package/src/lib/plugin-manager/plugin-contact-capture.js +24 -1
  72. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  73. package/src/lib/plugin-manager/plugin-customer-display-verifier.d.ts +77 -0
  74. package/src/lib/plugin-manager/plugin-customer-display-verifier.js +74 -0
  75. package/src/lib/plugin-manager/plugin-customer-display-verifier.js.map +1 -0
  76. package/src/lib/plugin-manager/plugin-customer-display.d.ts +135 -0
  77. package/src/lib/plugin-manager/plugin-customer-display.js +189 -0
  78. package/src/lib/plugin-manager/plugin-customer-display.js.map +1 -0
  79. package/src/lib/plugin-manager/plugin-entitlements.d.ts +59 -0
  80. package/src/lib/plugin-manager/plugin-entitlements.js +32 -3
  81. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  82. package/src/lib/plugin-manager/plugin-events.d.ts +45 -0
  83. package/src/lib/plugin-manager/plugin-events.js.map +1 -1
  84. package/src/lib/plugin-manager/plugin-permissions.d.ts +74 -0
  85. package/src/lib/plugin-manager/plugin-permissions.js +91 -1
  86. package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
  87. package/src/lib/plugin-manager/plugin-site-starters.d.ts +7 -0
  88. package/src/lib/plugin-manager/plugin-site-starters.js.map +1 -1
  89. package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +87 -4
  90. package/src/lib/plugin-manager/plugin-tax-return-sources.js +155 -35
  91. 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 { type OrgPermissionDefinition, type OrgPermissionGroup } from './org-permissions';
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
- return merged;
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
  }