@aglyn/aglyn 1.0.0-beta.268 → 1.0.0-beta.270

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 (82) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-preferences.d.ts +248 -0
  3. package/src/lib/app-utils/account-preferences.js +434 -0
  4. package/src/lib/app-utils/account-preferences.js.map +1 -0
  5. package/src/lib/app-utils/country-options.d.ts +27 -0
  6. package/src/lib/app-utils/country-options.js +96 -0
  7. package/src/lib/app-utils/country-options.js.map +1 -0
  8. package/src/lib/app-utils/docs-help.generated.d.ts +6 -0
  9. package/src/lib/app-utils/docs-help.generated.js +13 -0
  10. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  11. package/src/lib/app-utils/docs-index.generated.js +17 -10
  12. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  13. package/src/lib/app-utils/element-motion.d.ts +14 -2
  14. package/src/lib/app-utils/element-motion.js +33 -13
  15. package/src/lib/app-utils/element-motion.js.map +1 -1
  16. package/src/lib/app-utils/mobile-push.d.ts +8 -0
  17. package/src/lib/app-utils/mobile-push.js +4 -1
  18. package/src/lib/app-utils/mobile-push.js.map +1 -1
  19. package/src/lib/app-utils/native-app-association.d.ts +3 -2
  20. package/src/lib/app-utils/native-app-association.js +4 -2
  21. package/src/lib/app-utils/native-app-association.js.map +1 -1
  22. package/src/lib/app-utils/native-nav.d.ts +265 -0
  23. package/src/lib/app-utils/native-nav.js +741 -0
  24. package/src/lib/app-utils/native-nav.js.map +1 -0
  25. package/src/lib/app-utils/native-themes.d.ts +249 -0
  26. package/src/lib/app-utils/native-themes.js +732 -0
  27. package/src/lib/app-utils/native-themes.js.map +1 -0
  28. package/src/lib/app-utils/notifications.d.ts +1 -1
  29. package/src/lib/app-utils/notifications.js +10 -0
  30. package/src/lib/app-utils/notifications.js.map +1 -1
  31. package/src/lib/app-utils/one-box-search.d.ts +108 -0
  32. package/src/lib/app-utils/one-box-search.js +167 -0
  33. package/src/lib/app-utils/one-box-search.js.map +1 -0
  34. package/src/lib/app-utils/org-permissions.js +10 -61
  35. package/src/lib/app-utils/org-permissions.js.map +1 -1
  36. package/src/lib/app-utils/org-role-grants.d.ts +95 -0
  37. package/src/lib/app-utils/org-role-grants.js +193 -0
  38. package/src/lib/app-utils/org-role-grants.js.map +1 -0
  39. package/src/lib/app-utils/plan-entitlements.d.ts +41 -1
  40. package/src/lib/app-utils/plan-entitlements.js +65 -3
  41. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  42. package/src/lib/app-utils/tenant-dns-targets.d.ts +62 -0
  43. package/src/lib/app-utils/tenant-dns-targets.js +80 -0
  44. package/src/lib/app-utils/tenant-dns-targets.js.map +1 -0
  45. package/src/lib/foundation/definitions/org-billing.types.d.ts +14 -0
  46. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  47. package/src/lib/foundation/definitions/platform.types.d.ts +9 -0
  48. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  49. package/src/lib/plugin-manager/feature-plugins.d.ts +38 -0
  50. package/src/lib/plugin-manager/feature-plugins.js +20 -0
  51. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  52. package/src/lib/plugin-manager/first-party-plugins.generated.js +22 -1
  53. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  54. package/src/lib/plugin-manager/operator-alerts.js +10 -1
  55. package/src/lib/plugin-manager/operator-alerts.js.map +1 -1
  56. package/src/lib/plugin-manager/plugin-config.js +10 -1
  57. package/src/lib/plugin-manager/plugin-config.js.map +1 -1
  58. package/src/lib/plugin-manager/plugin-consent-group-change.js +11 -2
  59. package/src/lib/plugin-manager/plugin-consent-group-change.js.map +1 -1
  60. package/src/lib/plugin-manager/plugin-console-crons.js +10 -1
  61. package/src/lib/plugin-manager/plugin-console-crons.js.map +1 -1
  62. package/src/lib/plugin-manager/plugin-domain-events.d.ts +4 -3
  63. package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -1
  64. package/src/lib/plugin-manager/plugin-events.js +11 -2
  65. package/src/lib/plugin-manager/plugin-events.js.map +1 -1
  66. package/src/lib/plugin-manager/plugin-lead-conversion.js +10 -1
  67. package/src/lib/plugin-manager/plugin-lead-conversion.js.map +1 -1
  68. package/src/lib/plugin-manager/plugin-media-ingest.d.ts +25 -0
  69. package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -1
  70. package/src/lib/plugin-manager/plugin-membership-detach.js +10 -1
  71. package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
  72. package/src/lib/plugin-manager/plugin-person-matches.js +10 -1
  73. package/src/lib/plugin-manager/plugin-person-matches.js.map +1 -1
  74. package/src/lib/plugin-manager/plugin-search-sources.d.ts +85 -0
  75. package/src/lib/plugin-manager/plugin-search-sources.js +91 -0
  76. package/src/lib/plugin-manager/plugin-search-sources.js.map +1 -0
  77. package/src/lib/plugin-manager/plugin-services.js +6 -0
  78. package/src/lib/plugin-manager/plugin-services.js.map +1 -1
  79. package/src/lib/plugin-manager/plugin-usage-meters.js +10 -1
  80. package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -1
  81. package/src/lib/plugin-manager/usage-alert-contributors.js +10 -1
  82. package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type NativeNavManifest } from './native-nav';
18
+ /** The console route the apps call. */
19
+ export declare const ONE_BOX_SEARCH_PATH = "/api/search";
20
+ /** Rows per group when the request names no `limit`. */
21
+ export declare const ONE_BOX_ROWS_DEFAULT = 5;
22
+ /** The most rows one group answers in one response. */
23
+ export declare const ONE_BOX_ROWS_MAX = 25;
24
+ /** The longest query the route reads; the rest is dropped. */
25
+ export declare const ONE_BOX_QUERY_MAX = 100;
26
+ /** The group of workspaces the person belongs to: the switcher's half. */
27
+ export declare const ONE_BOX_WORKSPACES_GROUP = "workspaces";
28
+ /**
29
+ * The group of the app's own screens and settings. The apps answer it
30
+ * themselves, with no read, from the assembled manifest
31
+ * (`searchNavScreens(ContractValues.nativeNavManifest, …)`), and show it in
32
+ * the one box beside the route's groups.
33
+ */
34
+ export declare const ONE_BOX_SCREENS_GROUP = "appScreens";
35
+ /** One result. */
36
+ export interface OneBoxRow {
37
+ /** The group it came from (`workspaces`, `sites`, `screens`, `orders`, `contacts`, …). */
38
+ group: string;
39
+ /** The workspace id, site id, nav row id or record id. */
40
+ id: string;
41
+ label: string;
42
+ /** One line under it: a slug, a buyer, the list a setting is in. */
43
+ detail?: string;
44
+ /** The workspace it belongs to; a workspace row's own id. */
45
+ orgId?: string;
46
+ /** The site it belongs to; a site row's own id. */
47
+ hostId?: string;
48
+ /**
49
+ * The console path it opens at. The apps resolve it through their deep-link
50
+ * grammar, as they resolve a notification's link, so a record opens on its
51
+ * native screen and a Besigner page in the in-app Besigner.
52
+ */
53
+ link?: string;
54
+ /** For a screens-and-settings row: the native screen and its params. */
55
+ screen?: string;
56
+ params?: Record<string, string>;
57
+ }
58
+ /** One kind of result. */
59
+ export interface OneBoxGroup {
60
+ group: string;
61
+ /** The heading above its rows. */
62
+ heading: string;
63
+ /** The kind in a sentence ("pages"). */
64
+ noun: string;
65
+ rows: OneBoxRow[];
66
+ /** How many matched in what was searched, of which `rows` is a page. */
67
+ matched: number;
68
+ /** How many records were matched against. */
69
+ searched: number;
70
+ /** The group filled its window, so it may hold matches that were not read. */
71
+ truncated: boolean;
72
+ /** The group could not be read. Never the same as matching nothing. */
73
+ failed: boolean;
74
+ /** Pass back as `cursor` with `group` for the next page, absent at the end. */
75
+ nextCursor?: number;
76
+ }
77
+ export interface OneBoxResponse {
78
+ /** The query as it was read. */
79
+ query: string;
80
+ orgId?: string;
81
+ hostId?: string;
82
+ /** Groups in the order they are shown. */
83
+ groups: OneBoxGroup[];
84
+ }
85
+ /** How many rows a request asked for, within bounds. */
86
+ export declare function oneBoxLimit(raw: unknown): number;
87
+ /** Where a page starts, within bounds. */
88
+ export declare function oneBoxCursor(raw: unknown): number;
89
+ /** The query as the route reads it: trimmed, capped, or null when too short to run. */
90
+ export declare function oneBoxQuery(raw: unknown): string | null;
91
+ /** A matched row before paging, with its score. */
92
+ export interface OneBoxScored {
93
+ row: OneBoxRow;
94
+ score: number;
95
+ }
96
+ /**
97
+ * One group's page: every match ranked the console's way (score, then the
98
+ * shorter name, then A to Z), cut at `cursor` for `limit` rows.
99
+ */
100
+ export declare function pageOneBoxGroup(meta: Pick<OneBoxGroup, 'group' | 'heading' | 'noun' | 'searched' | 'truncated' | 'failed'>, scored: readonly OneBoxScored[], cursor: number, limit: number): OneBoxGroup;
101
+ /**
102
+ * The screens-and-settings matches for where the person stands, from the
103
+ * assembled manifest (`ContractValues.nativeNavManifest` in the apps): the
104
+ * workspace's lists with no site, the site's lists (and the workspace
105
+ * settings) with one. `enabledPluginIds` is the effective set for that
106
+ * scope, so a screen of a plugin that is off is never offered.
107
+ */
108
+ export declare function searchNavScreens(manifest: NativeNavManifest, text: string, scope: 'account' | 'workspace' | 'site', enabledPluginIds: readonly string[], hostAdmin: boolean): OneBoxScored[];
@@ -0,0 +1,167 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * THE ONE BOX: SWITCHING AND SEARCH IN ONE PLACE (AGL-3943, Zach 2026-10-18).
19
+ *
20
+ * ⌘K in the native apps opens one box that finds workspaces, sites, pages,
21
+ * orders, contacts, emails, components, layouts, templates and every plugin's
22
+ * records together, plus the app's own screens and settings ("billing" finds
23
+ * Plan). The apps ask `GET /api/search` (`ONE_BOX_SEARCH_PATH`) with the
24
+ * person's ID token and get grouped rows back; this module is that payload,
25
+ * the paging rules, and the screens-and-settings group, which the apps
26
+ * answer themselves from the nav manifest, with no read at all.
27
+ *
28
+ * Request query: `q` (at least two characters), `orgId` and `hostId` (the
29
+ * scope the person stands in; both optional), `limit` (rows per group,
30
+ * default 5, at most 25), and `group` + `cursor` to page one group further
31
+ * ("Show more"). Who may see what is decided by the route, never by these.
32
+ *
33
+ * Imports only the plain modules beside it, so the native apps read the
34
+ * types and the screens-and-settings answers through the generated contracts.
35
+ */ import { compareScored, isSearchableQuery, scoreMatch } from "./name-match.js";
36
+ import { resolveNavList } from "./native-nav.js";
37
+ /** The console route the apps call. */ export const ONE_BOX_SEARCH_PATH = '/api/search';
38
+ /** Rows per group when the request names no `limit`. */ export const ONE_BOX_ROWS_DEFAULT = 5;
39
+ /** The most rows one group answers in one response. */ export const ONE_BOX_ROWS_MAX = 25;
40
+ /** The longest query the route reads; the rest is dropped. */ export const ONE_BOX_QUERY_MAX = 100;
41
+ /** The group of workspaces the person belongs to: the switcher's half. */ export const ONE_BOX_WORKSPACES_GROUP = 'workspaces';
42
+ /**
43
+ * The group of the app's own screens and settings. The apps answer it
44
+ * themselves, with no read, from the assembled manifest
45
+ * (`searchNavScreens(ContractValues.nativeNavManifest, …)`), and show it in
46
+ * the one box beside the route's groups.
47
+ */ export const ONE_BOX_SCREENS_GROUP = 'appScreens';
48
+ /** How many rows a request asked for, within bounds. */ export function oneBoxLimit(raw) {
49
+ const asked = typeof raw === 'string' ? Number(raw) : typeof raw === 'number' ? raw : NaN;
50
+ if (!Number.isFinite(asked) || asked < 1) return ONE_BOX_ROWS_DEFAULT;
51
+ return Math.min(ONE_BOX_ROWS_MAX, Math.floor(asked));
52
+ }
53
+ /** Where a page starts, within bounds. */ export function oneBoxCursor(raw) {
54
+ const asked = typeof raw === 'string' ? Number(raw) : typeof raw === 'number' ? raw : NaN;
55
+ return Number.isFinite(asked) && asked > 0 ? Math.floor(asked) : 0;
56
+ }
57
+ /** The query as the route reads it: trimmed, capped, or null when too short to run. */ export function oneBoxQuery(raw) {
58
+ const text = typeof raw === 'string' ? raw.trim().slice(0, ONE_BOX_QUERY_MAX) : '';
59
+ return isSearchableQuery(text) ? text : null;
60
+ }
61
+ /**
62
+ * One group's page: every match ranked the console's way (score, then the
63
+ * shorter name, then A to Z), cut at `cursor` for `limit` rows.
64
+ */ export function pageOneBoxGroup(meta, scored, cursor, limit) {
65
+ const ranked = [
66
+ ...scored
67
+ ].sort((a, b)=>compareScored({
68
+ score: a.score,
69
+ label: a.row.label
70
+ }, {
71
+ score: b.score,
72
+ label: b.row.label
73
+ }));
74
+ const end = cursor + limit;
75
+ return _extends({}, meta, {
76
+ rows: ranked.slice(cursor, end).map((entry)=>entry.row),
77
+ matched: ranked.length
78
+ }, end < ranked.length ? {
79
+ nextCursor: end
80
+ } : {});
81
+ }
82
+ const LIST_HEADINGS = {
83
+ workspace: 'Workspace',
84
+ site: 'Site',
85
+ workspaceSettings: 'Workspace settings',
86
+ siteSettings: 'Site settings'
87
+ };
88
+ /**
89
+ * The screens-and-settings matches for where the person stands, from the
90
+ * assembled manifest (`ContractValues.nativeNavManifest` in the apps): the
91
+ * workspace's lists with no site, the site's lists (and the workspace
92
+ * settings) with one. `enabledPluginIds` is the effective set for that
93
+ * scope, so a screen of a plugin that is off is never offered.
94
+ */ export function searchNavScreens(manifest, text, scope, enabledPluginIds, hostAdmin) {
95
+ if (!isSearchableQuery(text) || scope === 'account') return [];
96
+ const lists = scope === 'site' ? [
97
+ 'site',
98
+ 'siteSettings',
99
+ 'workspaceSettings'
100
+ ] : [
101
+ 'workspace',
102
+ 'workspaceSettings'
103
+ ];
104
+ const seen = new Set();
105
+ const out = [];
106
+ for (const listId of lists){
107
+ const list = resolveNavList(manifest, listId, enabledPluginIds, hostAdmin);
108
+ for (const group of list.groups){
109
+ for (const row of group.rows){
110
+ var _row_keywords, _group_label, _row_sections;
111
+ const where = group.label ? `${LIST_HEADINGS[listId]} · ${group.label}` : LIST_HEADINGS[listId];
112
+ const entries = [
113
+ {
114
+ id: `${listId}.${row.id}`,
115
+ label: row.label,
116
+ screen: row.screen,
117
+ params: row.params,
118
+ keywords: [
119
+ ...(_row_keywords = row.keywords) != null ? _row_keywords : [],
120
+ (_group_label = group.label) != null ? _group_label : ''
121
+ ]
122
+ },
123
+ ...((_row_sections = row.sections) != null ? _row_sections : []).filter((section)=>section.label !== row.label).map((section)=>{
124
+ var _group_label;
125
+ return {
126
+ id: `${listId}.${row.id}.${section.id}`,
127
+ label: section.label,
128
+ screen: section.screen,
129
+ params: section.params,
130
+ keywords: [
131
+ row.label,
132
+ (_group_label = group.label) != null ? _group_label : ''
133
+ ]
134
+ };
135
+ })
136
+ ];
137
+ for (const entry of entries){
138
+ var _entry_params;
139
+ // The same screen reached from two lists is one result.
140
+ const key = `${entry.screen}|${JSON.stringify((_entry_params = entry.params) != null ? _entry_params : {})}`;
141
+ if (seen.has(key) || entry.screen === 'shell.home') continue;
142
+ const score = scoreMatch({
143
+ name: entry.label,
144
+ extra: entry.keywords
145
+ }, text);
146
+ if (score === null) continue;
147
+ seen.add(key);
148
+ out.push({
149
+ score,
150
+ row: _extends({
151
+ group: ONE_BOX_SCREENS_GROUP,
152
+ id: entry.id,
153
+ label: entry.label,
154
+ detail: entry.label === row.label ? where : `${where} · ${row.label}`,
155
+ screen: entry.screen
156
+ }, entry.params ? {
157
+ params: entry.params
158
+ } : {})
159
+ });
160
+ }
161
+ }
162
+ }
163
+ }
164
+ return out;
165
+ }
166
+
167
+ //# sourceMappingURL=one-box-search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/one-box-search.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\n/**\n * THE ONE BOX: SWITCHING AND SEARCH IN ONE PLACE (AGL-3943, Zach 2026-10-18).\n *\n * ⌘K in the native apps opens one box that finds workspaces, sites, pages,\n * orders, contacts, emails, components, layouts, templates and every plugin's\n * records together, plus the app's own screens and settings (\"billing\" finds\n * Plan). The apps ask `GET /api/search` (`ONE_BOX_SEARCH_PATH`) with the\n * person's ID token and get grouped rows back; this module is that payload,\n * the paging rules, and the screens-and-settings group, which the apps\n * answer themselves from the nav manifest, with no read at all.\n *\n * Request query: `q` (at least two characters), `orgId` and `hostId` (the\n * scope the person stands in; both optional), `limit` (rows per group,\n * default 5, at most 25), and `group` + `cursor` to page one group further\n * (\"Show more\"). Who may see what is decided by the route, never by these.\n *\n * Imports only the plain modules beside it, so the native apps read the\n * types and the screens-and-settings answers through the generated contracts.\n */\n\nimport { compareScored, isSearchableQuery, scoreMatch } from './name-match'\nimport { resolveNavList, type NativeNavListId, type NativeNavManifest } from './native-nav'\n\n/** The console route the apps call. */\nexport const ONE_BOX_SEARCH_PATH = '/api/search'\n\n/** Rows per group when the request names no `limit`. */\nexport const ONE_BOX_ROWS_DEFAULT = 5\n\n/** The most rows one group answers in one response. */\nexport const ONE_BOX_ROWS_MAX = 25\n\n/** The longest query the route reads; the rest is dropped. */\nexport const ONE_BOX_QUERY_MAX = 100\n\n/** The group of workspaces the person belongs to: the switcher's half. */\nexport const ONE_BOX_WORKSPACES_GROUP = 'workspaces'\n\n/**\n * The group of the app's own screens and settings. The apps answer it\n * themselves, with no read, from the assembled manifest\n * (`searchNavScreens(ContractValues.nativeNavManifest, …)`), and show it in\n * the one box beside the route's groups.\n */\nexport const ONE_BOX_SCREENS_GROUP = 'appScreens'\n\n/** One result. */\nexport interface OneBoxRow {\n /** The group it came from (`workspaces`, `sites`, `screens`, `orders`, `contacts`, …). */\n group: string\n /** The workspace id, site id, nav row id or record id. */\n id: string\n label: string\n /** One line under it: a slug, a buyer, the list a setting is in. */\n detail?: string\n /** The workspace it belongs to; a workspace row's own id. */\n orgId?: string\n /** The site it belongs to; a site row's own id. */\n hostId?: string\n /**\n * The console path it opens at. The apps resolve it through their deep-link\n * grammar, as they resolve a notification's link, so a record opens on its\n * native screen and a Besigner page in the in-app Besigner.\n */\n link?: string\n /** For a screens-and-settings row: the native screen and its params. */\n screen?: string\n params?: Record<string, string>\n}\n\n/** One kind of result. */\nexport interface OneBoxGroup {\n group: string\n /** The heading above its rows. */\n heading: string\n /** The kind in a sentence (\"pages\"). */\n noun: string\n rows: OneBoxRow[]\n /** How many matched in what was searched, of which `rows` is a page. */\n matched: number\n /** How many records were matched against. */\n searched: number\n /** The group filled its window, so it may hold matches that were not read. */\n truncated: boolean\n /** The group could not be read. Never the same as matching nothing. */\n failed: boolean\n /** Pass back as `cursor` with `group` for the next page, absent at the end. */\n nextCursor?: number\n}\n\nexport interface OneBoxResponse {\n /** The query as it was read. */\n query: string\n orgId?: string\n hostId?: string\n /** Groups in the order they are shown. */\n groups: OneBoxGroup[]\n}\n\n/** How many rows a request asked for, within bounds. */\nexport function oneBoxLimit(raw: unknown): number {\n const asked = typeof raw === 'string' ? Number(raw) : typeof raw === 'number' ? raw : NaN\n if (!Number.isFinite(asked) || asked < 1) return ONE_BOX_ROWS_DEFAULT\n return Math.min(ONE_BOX_ROWS_MAX, Math.floor(asked))\n}\n\n/** Where a page starts, within bounds. */\nexport function oneBoxCursor(raw: unknown): number {\n const asked = typeof raw === 'string' ? Number(raw) : typeof raw === 'number' ? raw : NaN\n return Number.isFinite(asked) && asked > 0 ? Math.floor(asked) : 0\n}\n\n/** The query as the route reads it: trimmed, capped, or null when too short to run. */\nexport function oneBoxQuery(raw: unknown): string | null {\n const text = typeof raw === 'string' ? raw.trim().slice(0, ONE_BOX_QUERY_MAX) : ''\n return isSearchableQuery(text) ? text : null\n}\n\n/** A matched row before paging, with its score. */\nexport interface OneBoxScored {\n row: OneBoxRow\n score: number\n}\n\n/**\n * One group's page: every match ranked the console's way (score, then the\n * shorter name, then A to Z), cut at `cursor` for `limit` rows.\n */\nexport function pageOneBoxGroup(\n meta: Pick<OneBoxGroup, 'group' | 'heading' | 'noun' | 'searched' | 'truncated' | 'failed'>,\n scored: readonly OneBoxScored[],\n cursor: number,\n limit: number,\n): OneBoxGroup {\n const ranked = [...scored].sort((a, b) =>\n compareScored({ score: a.score, label: a.row.label }, { score: b.score, label: b.row.label }),\n )\n const end = cursor + limit\n return {\n ...meta,\n rows: ranked.slice(cursor, end).map((entry) => entry.row),\n matched: ranked.length,\n ...(end < ranked.length ? { nextCursor: end } : {}),\n }\n}\n\nconst LIST_HEADINGS: Record<NativeNavListId, string> = {\n workspace: 'Workspace',\n site: 'Site',\n workspaceSettings: 'Workspace settings',\n siteSettings: 'Site settings',\n}\n\n/**\n * The screens-and-settings matches for where the person stands, from the\n * assembled manifest (`ContractValues.nativeNavManifest` in the apps): the\n * workspace's lists with no site, the site's lists (and the workspace\n * settings) with one. `enabledPluginIds` is the effective set for that\n * scope, so a screen of a plugin that is off is never offered.\n */\nexport function searchNavScreens(\n manifest: NativeNavManifest,\n text: string,\n scope: 'account' | 'workspace' | 'site',\n enabledPluginIds: readonly string[],\n hostAdmin: boolean,\n): OneBoxScored[] {\n if (!isSearchableQuery(text) || scope === 'account') return []\n const lists: NativeNavListId[] =\n scope === 'site' ? ['site', 'siteSettings', 'workspaceSettings'] : ['workspace', 'workspaceSettings']\n const seen = new Set<string>()\n const out: OneBoxScored[] = []\n for (const listId of lists) {\n const list = resolveNavList(manifest, listId, enabledPluginIds, hostAdmin)\n for (const group of list.groups) {\n for (const row of group.rows) {\n const where = group.label ? `${LIST_HEADINGS[listId]} · ${group.label}` : LIST_HEADINGS[listId]\n const entries = [\n {\n id: `${listId}.${row.id}`,\n label: row.label,\n screen: row.screen,\n params: row.params,\n keywords: [...(row.keywords ?? []), group.label ?? ''],\n },\n ...(row.sections ?? [])\n .filter((section) => section.label !== row.label)\n .map((section) => ({\n id: `${listId}.${row.id}.${section.id}`,\n label: section.label,\n screen: section.screen,\n params: section.params,\n keywords: [row.label, group.label ?? ''],\n })),\n ]\n for (const entry of entries) {\n // The same screen reached from two lists is one result.\n const key = `${entry.screen}|${JSON.stringify(entry.params ?? {})}`\n if (seen.has(key) || entry.screen === 'shell.home') continue\n const score = scoreMatch({ name: entry.label, extra: entry.keywords }, text)\n if (score === null) continue\n seen.add(key)\n out.push({\n score,\n row: {\n group: ONE_BOX_SCREENS_GROUP,\n id: entry.id,\n label: entry.label,\n detail: entry.label === row.label ? where : `${where} · ${row.label}`,\n screen: entry.screen,\n ...(entry.params ? { params: entry.params } : {}),\n },\n })\n }\n }\n }\n }\n return out\n}\n"],"names":["compareScored","isSearchableQuery","scoreMatch","resolveNavList","ONE_BOX_SEARCH_PATH","ONE_BOX_ROWS_DEFAULT","ONE_BOX_ROWS_MAX","ONE_BOX_QUERY_MAX","ONE_BOX_WORKSPACES_GROUP","ONE_BOX_SCREENS_GROUP","oneBoxLimit","raw","asked","Number","NaN","isFinite","Math","min","floor","oneBoxCursor","oneBoxQuery","text","trim","slice","pageOneBoxGroup","meta","scored","cursor","limit","ranked","sort","a","b","score","label","row","end","rows","map","entry","matched","length","nextCursor","LIST_HEADINGS","workspace","site","workspaceSettings","siteSettings","searchNavScreens","manifest","scope","enabledPluginIds","hostAdmin","lists","seen","Set","out","listId","list","group","groups","where","entries","id","screen","params","keywords","sections","filter","section","key","JSON","stringify","has","name","extra","add","push","detail"],"mappings":"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"}
@@ -16,6 +16,7 @@
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  import { listPluginOrgPermissions, subscribePluginEntitlements } from "../plugin-manager/plugin-entitlements.js";
18
18
  import { ORG_ROLE_PERMISSION_KEYS } from "./org-roles.js";
19
+ import { CORE_ORG_PERMISSION_ENTRIES, CORE_ORG_ROLE_GRANTS } from "./org-role-grants.js";
19
20
  /**
20
21
  * The core keys, in display order for role editors.
21
22
  *
@@ -37,69 +38,17 @@ import { ORG_ROLE_PERMISSION_KEYS } from "./org-roles.js";
37
38
  *
38
39
  * `apps/console/specs/org-permissions-are-enforced.spec.ts` fails the build if
39
40
  * a key here, or a key a plugin declares, gains no server-side consumer.
40
- */ const CORE_ORG_PERMISSIONS = [
41
- {
42
- key: 'org.settings',
43
- label: 'Organization settings',
44
- description: 'Rename the organization, change its slug, transfer it.'
45
- },
46
- {
47
- key: 'org.auditLog',
48
- label: 'Activity & audit log',
49
- description: 'See the organization activity feed.'
50
- },
51
- {
52
- key: 'billing.view',
53
- label: 'View billing',
54
- description: 'See the plan, usage meters, and invoices.'
55
- },
56
- {
57
- key: 'billing.manage',
58
- label: 'Manage billing',
59
- description: 'Change plans, buy add-ons, update payment details.'
60
- },
61
- {
62
- key: 'members.manage',
63
- label: 'Manage members',
64
- description: 'Invite, remove, and re-role organization members.'
65
- },
66
- {
67
- key: 'hosts.create',
68
- label: 'Create sites',
69
- description: 'Create new sites in the organization.'
70
- },
71
- {
72
- key: 'hosts.delete',
73
- label: 'Delete sites',
74
- description: 'Delete sites the member can access.'
75
- },
76
- {
77
- key: 'data.manage',
78
- label: 'Manage data',
79
- description: 'Create, edit, and delete organization datasets.'
80
- },
81
- {
82
- key: 'marketplace.publish',
83
- label: 'Publish to marketplace',
84
- description: 'Publish listings under the organization profile.'
85
- },
86
- {
87
- key: 'plugins.install',
88
- label: 'Install plugins',
89
- description: 'Install or remove marketplace plugins.'
90
- }
91
- ];
41
+ */ const CORE_ORG_PERMISSIONS = // One table, shared with the native apps through the contracts (AGL-3950).
42
+ CORE_ORG_PERMISSION_ENTRIES.map((entry)=>_extends({}, entry, {
43
+ key: entry.key
44
+ }));
92
45
  /** The core keys: a plugin may add keys beside them and never redefine one. */ const CORE_ORG_PERMISSION_KEYS = new Set(CORE_ORG_PERMISSIONS.map((definition)=>definition.key));
93
46
  /** The legacy camelCase projection's keys, which no declaration may take either. */ const LEGACY_PERMISSION_KEYS = new Set(ORG_ROLE_PERMISSION_KEYS);
94
- /** The core keys each org role holds when nothing refines it. */ const CORE_ROLE_GRANTS = {
95
- owner: CORE_ORG_PERMISSION_KEYS,
96
- admin: CORE_ORG_PERMISSION_KEYS,
97
- editor: new Set([
98
- 'data.manage',
99
- 'marketplace.publish',
100
- 'plugins.install'
101
- ]),
102
- viewer: new Set()
47
+ /** The core keys each org role holds when nothing refines it (`org-role-grants.ts`). */ const CORE_ROLE_GRANTS = {
48
+ owner: new Set(CORE_ORG_ROLE_GRANTS.owner),
49
+ admin: new Set(CORE_ORG_ROLE_GRANTS.admin),
50
+ editor: new Set(CORE_ORG_ROLE_GRANTS.editor),
51
+ viewer: new Set(CORE_ORG_ROLE_GRANTS.viewer)
103
52
  };
104
53
  const ORG_ROLES = [
105
54
  'owner',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/org-permissions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynOrgMember, OrgRole } from '../foundation'\nimport {\n listPluginOrgPermissions,\n subscribePluginEntitlements,\n} from '../plugin-manager/plugin-entitlements'\nimport { ORG_ROLE_PERMISSION_KEYS } from './org-roles'\n\n/**\n * Granular org permissions (AGL-243) layered on the 4 org roles: each\n * role maps to a default permission set; custom roles\n * (`orgs/{orgId}/roles/{roleId}`) and per-member overrides refine it, so\n * e.g. an editor restricted to one host can also be granted billing\n * visibility — or an admin stripped of it — without inventing new roles.\n * The console hides unpermitted surfaces; the org API routes are the\n * enforcement point.\n */\nexport type CoreOrgPermission =\n | 'org.settings'\n | 'org.auditLog'\n | 'billing.view'\n | 'billing.manage'\n | 'members.manage'\n | 'hosts.create'\n | 'hosts.delete'\n | 'data.manage'\n | 'marketplace.publish'\n | 'plugins.install'\n\n/**\n * A key of the catalog: a core key above, or a key a plugin declares into\n * the catalog through `registerPluginEntitlements` (AGL-2984). A declared key\n * is stored on custom roles and member overrides, layered and resolved\n * exactly like a core key.\n */\nexport type OrgPermission = CoreOrgPermission | (string & {})\n\n/**\n * Legacy boolean permission map derived from the granular `OrgPermission`\n * set (AGL-243). Kept for the console surfaces that predate the granular\n * model; lives here so relocated feature plugins can accept it as a prop\n * (AGL-395).\n */\nexport interface OrgPermissions {\n createHosts: boolean\n editHosts: boolean\n editBilling: boolean\n publishToMarketplace: boolean\n installPlugins: boolean\n manageMembers: boolean\n}\n\nexport interface OrgPermissionDefinition {\n key: OrgPermission\n label: string\n description: string\n /** The plugin that declared the key; absent for a core key. */\n pluginId?: string\n}\n\n/**\n * The core keys, in display order for role editors.\n *\n * ⚠️ **EVERY KEY HERE MUST BE ENFORCED SERVER-SIDE.** A permission a customer\n * can tick that changes nothing is worse than its absence, because it implies\n * a control that does not exist — an owner unticks \"delete sites\", hands the\n * role out, and the member deletes sites while the console does not even dim\n * the button. Three of the eleven were in exactly that state (AGL-2444).\n *\n * `marketing.manage` was removed rather than wired, and the reason is worth\n * keeping: announcement bars, popups and campaigns live at\n * `hosts/{hostId}/overlays|campaigns|experiments` and are written\n * client-direct against the security rules, which gate on the HOST role.\n * There is no org-level boundary for it to sit on, and the action it named is\n * not org-scoped at all. Wiring it would have meant inventing one and\n * producing a second permission that looks enforced and is not. If a granular\n * marketing permission is wanted it belongs in the plugin-declared mechanism\n * (AGL-435), scoped to a host — a product decision, not this repair.\n *\n * `apps/console/specs/org-permissions-are-enforced.spec.ts` fails the build if\n * a key here, or a key a plugin declares, gains no server-side consumer.\n */\nconst CORE_ORG_PERMISSIONS: readonly OrgPermissionDefinition[] = [\n {\n key: 'org.settings',\n label: 'Organization settings',\n description: 'Rename the organization, change its slug, transfer it.',\n },\n {\n key: 'org.auditLog',\n label: 'Activity & audit log',\n description: 'See the organization activity feed.',\n },\n {\n key: 'billing.view',\n label: 'View billing',\n description: 'See the plan, usage meters, and invoices.',\n },\n {\n key: 'billing.manage',\n label: 'Manage billing',\n description: 'Change plans, buy add-ons, update payment details.',\n },\n {\n key: 'members.manage',\n label: 'Manage members',\n description: 'Invite, remove, and re-role organization members.',\n },\n {\n key: 'hosts.create',\n label: 'Create sites',\n description: 'Create new sites in the organization.',\n },\n {\n key: 'hosts.delete',\n label: 'Delete sites',\n description: 'Delete sites the member can access.',\n },\n {\n key: 'data.manage',\n label: 'Manage data',\n description: 'Create, edit, and delete organization datasets.',\n },\n {\n key: 'marketplace.publish',\n label: 'Publish to marketplace',\n description: 'Publish listings under the organization profile.',\n },\n {\n key: 'plugins.install',\n label: 'Install plugins',\n description: 'Install or remove marketplace plugins.',\n },\n]\n\n/** The core keys: a plugin may add keys beside them and never redefine one. */\nconst CORE_ORG_PERMISSION_KEYS: ReadonlySet<string> = new Set(\n CORE_ORG_PERMISSIONS.map((definition) => definition.key),\n)\n\n/** The legacy camelCase projection's keys, which no declaration may take either. */\nconst LEGACY_PERMISSION_KEYS: ReadonlySet<string> = new Set<string>(\n ORG_ROLE_PERMISSION_KEYS,\n)\n\n/** The core keys each org role holds when nothing refines it. */\nconst CORE_ROLE_GRANTS: Record<OrgRole, ReadonlySet<string>> = {\n owner: CORE_ORG_PERMISSION_KEYS,\n admin: CORE_ORG_PERMISSION_KEYS,\n editor: new Set<CoreOrgPermission>([\n 'data.manage',\n 'marketplace.publish',\n 'plugins.install',\n ]),\n viewer: new Set<string>(),\n}\n\nconst ORG_ROLES: readonly OrgRole[] = ['owner', 'admin', 'editor', 'viewer']\n\n/**\n * Every permission, in display order for role editors: the core keys, then\n * the keys plugins declare through `registerPluginEntitlements`, in the\n * plugins' catalog order (AGL-2984).\n *\n * LIVE, and kept in step in place. A plugin's declarations register at\n * module scope — at boot on the server, before the first paint in the\n * console — which can be after a module holding this binding was evaluated.\n * So this array, the key list, the role defaults and the empty map below are\n * rewritten in place on every registration instead of being computed once\n * when this module loads, and whoever holds the binding reads the declared\n * keys with the core ones.\n */\nexport const ORG_PERMISSIONS: readonly OrgPermissionDefinition[] = []\n\nexport const ORG_PERMISSION_KEYS: readonly OrgPermission[] = []\n\nconst NO_PERMISSIONS = {} as Record<OrgPermission, boolean>\n\n/**\n * Role → default permission set. Owner/admin hold everything (owner-only\n * actions like org deletion stay role-checked, not permission-checked);\n * editors work on content but see no money or roster controls; viewers read\n * only. A declared key's default for each role is the one its declaration\n * names.\n */\nexport const DEFAULT_ROLE_PERMISSIONS: Record<\n OrgRole,\n Record<OrgPermission, boolean>\n> = {\n owner: {} as Record<OrgPermission, boolean>,\n admin: {} as Record<OrgPermission, boolean>,\n editor: {} as Record<OrgPermission, boolean>,\n viewer: {} as Record<OrgPermission, boolean>,\n}\n\n/** Refused declarations already logged, so a refusal is reported once. */\nconst reportedRefusals = new Set<string>()\n\nfunction replaceItems<T>(target: readonly T[], items: readonly T[]): void {\n const list = target as T[]\n list.length = 0\n list.push(...items)\n}\n\nfunction replaceEntries(\n target: Record<string, boolean>,\n entries: Record<string, boolean>,\n): void {\n for (const key of Object.keys(target)) delete target[key]\n Object.assign(target, entries)\n}\n\n/**\n * Rebuilds the live catalog: the core keys, then every key a plugin declared.\n * A declaration naming a core or legacy key is refused — that key keeps its\n * own meaning and defaults — and logged once.\n */\nfunction syncCatalog(): void {\n const definitions: OrgPermissionDefinition[] = [...CORE_ORG_PERMISSIONS]\n const declaredDefaults = new Map<string, Readonly<Record<OrgRole, boolean>>>()\n for (const declared of listPluginOrgPermissions()) {\n if (\n CORE_ORG_PERMISSION_KEYS.has(declared.key) ||\n LEGACY_PERMISSION_KEYS.has(declared.key)\n ) {\n const refusal = `${declared.pluginId}\\x00${declared.key}`\n if (!reportedRefusals.has(refusal)) {\n reportedRefusals.add(refusal)\n console.error(\n `[plugins] refused catalog permission \"${declared.key}\" to ` +\n `\"${declared.pluginId}\": the key belongs to the core catalog`,\n )\n }\n continue\n }\n definitions.push({\n key: declared.key,\n label: declared.label,\n description: declared.description,\n pluginId: declared.pluginId,\n })\n declaredDefaults.set(declared.key, declared.roleDefaults)\n }\n const keys = definitions.map((definition) => definition.key)\n replaceItems(ORG_PERMISSIONS, definitions)\n replaceItems(ORG_PERMISSION_KEYS, keys)\n replaceEntries(NO_PERMISSIONS, Object.fromEntries(keys.map((key) => [key, false])))\n for (const role of ORG_ROLES) {\n replaceEntries(\n DEFAULT_ROLE_PERMISSIONS[role],\n Object.fromEntries(\n keys.map((key) => [\n key,\n declaredDefaults.has(key)\n ? declaredDefaults.get(key)?.[role] === true\n : CORE_ROLE_GRANTS[role].has(key),\n ]),\n ),\n )\n }\n CATALOG_AND_LEGACY_KEYS.clear()\n for (const key of [...keys, ...LEGACY_PERMISSION_KEYS]) {\n CATALOG_AND_LEGACY_KEYS.add(key)\n }\n}\n\n/** `orgs/{orgId}/roles/{roleId}` — a named custom permission set. */\nexport interface AglynOrgCustomRole {\n $id?: string\n name?: string\n description?: string\n /** Full permission map; missing keys read as false. */\n permissions?: Partial<Record<OrgPermission, boolean>>\n}\n\n/**\n * Effective permission map for a member: the org role's defaults, then\n * the custom role's map (when the member has `roleId` and the role doc\n * is supplied), then per-member overrides — later layers win key-by-key.\n */\nexport function resolveOrgPermissions(\n member:\n | (Partial<AglynOrgMember> & {\n roleId?: string\n permissions?: Partial<Record<OrgPermission, boolean>>\n })\n | null\n | undefined,\n customRole?: AglynOrgCustomRole | null,\n): Record<OrgPermission, boolean> {\n if (!member) return NO_PERMISSIONS\n const role = (member.role ?? 'viewer') as OrgRole\n const base = DEFAULT_ROLE_PERMISSIONS[role] ?? NO_PERMISSIONS\n const merged: Record<OrgPermission, boolean> = { ...base }\n if (member.roleId && customRole?.permissions) {\n for (const key of ORG_PERMISSION_KEYS) {\n const value = customRole.permissions[key]\n if (typeof value === 'boolean') merged[key] = value\n }\n }\n if (member.permissions) {\n for (const key of ORG_PERMISSION_KEYS) {\n const value = member.permissions[key]\n if (typeof value === 'boolean') merged[key] = value\n }\n }\n return merged\n}\n\n/**\n * The values a custom role and a per-member override set EXPLICITLY for\n * permission keys outside this catalog — the keys plugins declare through\n * `registerPluginPermissions` (AGL-2974).\n *\n * `resolveOrgPermissions` walks the catalog alone, so a plugin key stored on\n * a custom role or a member document never reached the `resolvedPermissions`\n * stamp the Firestore rules read. A rule gating on one could then honor\n * neither a grant to an editor nor a revocation from an admin. This carries\n * exactly those stored decisions, custom role first and the per-member\n * override over it, which is the precedence the resolver applies.\n *\n * Tier DEFAULTS are deliberately absent. They live in the plugin registry,\n * which is populated per process by whichever plugin bundles that process\n * loaded, and a stamp computed from it would differ by which route happened\n * to write it. A rule supplies the default itself from the member's role,\n * the way `memberResolves` is always conjoined with a role gate.\n *\n * The legacy six camelCase keys are excluded as well: they are the catalog's\n * older projection, not plugin keys, and no rule reads them.\n */\nexport function explicitPluginPermissionValues(\n member:\n | {\n roleId?: string\n permissions?: Partial<Record<string, unknown>> | null\n }\n | null\n | undefined,\n customRole?: { permissions?: Partial<Record<string, unknown>> | null } | null,\n): Record<string, boolean> {\n const values: Record<string, boolean> = {}\n if (!member) return values\n const layers = [\n member.roleId ? customRole?.permissions : undefined,\n member.permissions,\n ]\n for (const layer of layers) {\n for (const [key, value] of Object.entries(layer ?? {})) {\n if (typeof value !== 'boolean') continue\n if (CATALOG_AND_LEGACY_KEYS.has(key)) continue\n values[key] = value\n }\n }\n return values\n}\n\n/**\n * Keys {@link explicitPluginPermissionValues} leaves to the catalog: every\n * catalog key, declared ones included, and the legacy six. Kept in step by\n * `syncCatalog`.\n */\nconst CATALOG_AND_LEGACY_KEYS = new Set<string>()\n\n/**\n * The `resolvedPermissions` map stamped on a member document for the\n * Firestore rules: the catalog verdict with every layer applied, and beside\n * it the plugin keys a custom role or an override set explicitly.\n *\n * One function for every writer of the stamp, so the org-create transaction\n * and `syncOrgAuthProjections` cannot project different maps. The catalog\n * spreads last; the two key spaces do not overlap, and the order keeps it\n * true that nothing but the resolver decides a catalog key.\n */\nexport function projectMemberResolvedPermissions(\n member: Parameters<typeof resolveOrgPermissions>[0],\n customRole?: AglynOrgCustomRole | null,\n): Record<string, boolean> {\n return {\n ...explicitPluginPermissionValues(member, customRole),\n ...resolveOrgPermissions(member, customRole),\n }\n}\n\n/** Single-permission convenience over `resolveOrgPermissions`. */\nexport function hasOrgPermission(\n member: Parameters<typeof resolveOrgPermissions>[0],\n permission: OrgPermission,\n customRole?: AglynOrgCustomRole | null,\n): boolean {\n return resolveOrgPermissions(member, customRole)[permission]\n}\n\n/** The catalog label for a key — the words a refusal names — or the key itself. */\nexport function orgPermissionLabel(permission: OrgPermission): string {\n return (\n ORG_PERMISSIONS.find((definition) => definition.key === permission)?.label ??\n permission\n )\n}\n\n/** The keys plugins declared into the catalog, in catalog order. */\nexport function pluginOrgPermissionKeys(): OrgPermission[] {\n return ORG_PERMISSIONS.filter((definition) => definition.pluginId).map(\n (definition) => definition.key,\n )\n}\n\n/** One declared catalog key that moved, and the direction it moved in. */\nexport interface PluginPermissionChange {\n permission: string\n granted: boolean\n}\n\n/**\n * The keys PLUGINS declared into the catalog whose value moved between two\n * maps, in the new map's order — what a member, role or collaborator write\n * raises `org.permissions.changed` for, once per key, so the plugin that owns\n * a key can record who moved it.\n *\n * A key the new map leaves unset is not a change: on a role it defers to the\n * base role, on a site toggle to the host role, and neither is a grant or a\n * revocation of its own. A key set for the first time is a change, because\n * the verdict it produces is no longer the default's. The core keys and the\n * legacy six raise nothing; their writes are recorded by the routes' own\n * activity sentences.\n */\nexport function pluginPermissionChanges(\n before: Partial<Record<string, unknown>> | null | undefined,\n after: Partial<Record<string, unknown>> | null | undefined,\n): PluginPermissionChange[] {\n const changes: PluginPermissionChange[] = []\n for (const [key, next] of Object.entries(after ?? {})) {\n if (typeof next !== 'boolean') continue\n if (CORE_ORG_PERMISSION_KEYS.has(key) || LEGACY_PERMISSION_KEYS.has(key)) continue\n if (before?.[key] === next) continue\n changes.push({ permission: key, granted: next })\n }\n return changes\n}\n\n/**\n * The granular (dotted) set projected onto the legacy boolean map\n * (AGL-2350).\n *\n * ## Why this lives here rather than in the console hook\n *\n * It was a private function in `apps/console/hooks/use-org-permissions.ts`,\n * which meant the CLIENT translated the stored permission model into the\n * legacy flags and the SERVER did not translate at all — it derived the\n * legacy flags straight from the built-in role tier and dropped custom roles\n * and per-member overrides on the floor. The two disagreed exactly on the\n * feature `custom-roles.md` sells, in both directions: a permission granted\n * by a custom role showed in the UI and 403'd on POST, and one revoked by an\n * override was hidden in the UI and still succeeded on POST.\n *\n * One exported copy is what stops them drifting again.\n *\n * ## The key spaces are NOT interchangeable, and that is the trap\n *\n * Two permission vocabularies exist. The stored one is DOTTED\n * (`plugins.install`) — `apps/console/app/api/orgs/roles/route.ts` sanitizes\n * against `ORG_PERMISSION_KEYS` before writing, and `AglynOrgMember`\n * `permissions` is typed to it. The legacy one is camelCase\n * (`installPlugins`).\n *\n * `resolveRolePermissions` in `org-roles.ts` accepts `overrides` and\n * `customRoles` arguments keyed by the CAMELCASE space. Feeding it the real\n * stored documents therefore matches no key and changes nothing — it looks\n * like wiring the feature up while doing precisely nothing. Those two\n * parameters are used by nothing but that module's own spec. Translate\n * through here instead.\n *\n * `editHosts` is derived from the ROLE, not from a dotted key: it has no\n * counterpart in the granular catalog and never had one.\n */\nexport function toLegacyPermissions(\n granted: Record<OrgPermission, boolean>,\n role: OrgRole | null | undefined,\n): OrgPermissions {\n return {\n createHosts: granted['hosts.create'],\n editHosts: role !== 'viewer',\n editBilling: granted['billing.manage'],\n publishToMarketplace: granted['marketplace.publish'],\n installPlugins: granted['plugins.install'],\n manageMembers: granted['members.manage'],\n }\n}\n\nsyncCatalog()\nsubscribePluginEntitlements(syncCatalog)\n"],"names":["listPluginOrgPermissions","subscribePluginEntitlements","ORG_ROLE_PERMISSION_KEYS","CORE_ORG_PERMISSIONS","key","label","description","CORE_ORG_PERMISSION_KEYS","Set","map","definition","LEGACY_PERMISSION_KEYS","CORE_ROLE_GRANTS","owner","admin","editor","viewer","ORG_ROLES","ORG_PERMISSIONS","ORG_PERMISSION_KEYS","NO_PERMISSIONS","DEFAULT_ROLE_PERMISSIONS","reportedRefusals","replaceItems","target","items","list","length","push","replaceEntries","entries","Object","keys","assign","syncCatalog","definitions","declaredDefaults","Map","declared","has","refusal","pluginId","add","console","error","set","roleDefaults","fromEntries","role","get","CATALOG_AND_LEGACY_KEYS","clear","resolveOrgPermissions","member","customRole","base","merged","roleId","permissions","value","explicitPluginPermissionValues","values","layers","undefined","layer","projectMemberResolvedPermissions","hasOrgPermission","permission","orgPermissionLabel","find","pluginOrgPermissionKeys","filter","pluginPermissionChanges","before","after","changes","next","granted","toLegacyPermissions","createHosts","editHosts","editBilling","publishToMarketplace","installPlugins","manageMembers"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/org-permissions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynOrgMember, OrgRole } from '../foundation'\nimport {\n listPluginOrgPermissions,\n subscribePluginEntitlements,\n} from '../plugin-manager/plugin-entitlements'\nimport { ORG_ROLE_PERMISSION_KEYS } from './org-roles'\nimport { CORE_ORG_PERMISSION_ENTRIES, CORE_ORG_ROLE_GRANTS } from './org-role-grants'\n\n/**\n * Granular org permissions (AGL-243) layered on the 4 org roles: each\n * role maps to a default permission set; custom roles\n * (`orgs/{orgId}/roles/{roleId}`) and per-member overrides refine it, so\n * e.g. an editor restricted to one host can also be granted billing\n * visibility — or an admin stripped of it — without inventing new roles.\n * The console hides unpermitted surfaces; the org API routes are the\n * enforcement point.\n */\nexport type CoreOrgPermission =\n | 'org.settings'\n | 'org.auditLog'\n | 'billing.view'\n | 'billing.manage'\n | 'members.manage'\n | 'hosts.create'\n | 'hosts.delete'\n | 'data.manage'\n | 'marketplace.publish'\n | 'plugins.install'\n\n/**\n * A key of the catalog: a core key above, or a key a plugin declares into\n * the catalog through `registerPluginEntitlements` (AGL-2984). A declared key\n * is stored on custom roles and member overrides, layered and resolved\n * exactly like a core key.\n */\nexport type OrgPermission = CoreOrgPermission | (string & {})\n\n/**\n * Legacy boolean permission map derived from the granular `OrgPermission`\n * set (AGL-243). Kept for the console surfaces that predate the granular\n * model; lives here so relocated feature plugins can accept it as a prop\n * (AGL-395).\n */\nexport interface OrgPermissions {\n createHosts: boolean\n editHosts: boolean\n editBilling: boolean\n publishToMarketplace: boolean\n installPlugins: boolean\n manageMembers: boolean\n}\n\nexport interface OrgPermissionDefinition {\n key: OrgPermission\n label: string\n description: string\n /** The plugin that declared the key; absent for a core key. */\n pluginId?: string\n}\n\n/**\n * The core keys, in display order for role editors.\n *\n * ⚠️ **EVERY KEY HERE MUST BE ENFORCED SERVER-SIDE.** A permission a customer\n * can tick that changes nothing is worse than its absence, because it implies\n * a control that does not exist — an owner unticks \"delete sites\", hands the\n * role out, and the member deletes sites while the console does not even dim\n * the button. Three of the eleven were in exactly that state (AGL-2444).\n *\n * `marketing.manage` was removed rather than wired, and the reason is worth\n * keeping: announcement bars, popups and campaigns live at\n * `hosts/{hostId}/overlays|campaigns|experiments` and are written\n * client-direct against the security rules, which gate on the HOST role.\n * There is no org-level boundary for it to sit on, and the action it named is\n * not org-scoped at all. Wiring it would have meant inventing one and\n * producing a second permission that looks enforced and is not. If a granular\n * marketing permission is wanted it belongs in the plugin-declared mechanism\n * (AGL-435), scoped to a host — a product decision, not this repair.\n *\n * `apps/console/specs/org-permissions-are-enforced.spec.ts` fails the build if\n * a key here, or a key a plugin declares, gains no server-side consumer.\n */\nconst CORE_ORG_PERMISSIONS: readonly OrgPermissionDefinition[] =\n // One table, shared with the native apps through the contracts (AGL-3950).\n CORE_ORG_PERMISSION_ENTRIES.map((entry) => ({ ...entry, key: entry.key as CoreOrgPermission }))\n\n/** The core keys: a plugin may add keys beside them and never redefine one. */\nconst CORE_ORG_PERMISSION_KEYS: ReadonlySet<string> = new Set(\n CORE_ORG_PERMISSIONS.map((definition) => definition.key),\n)\n\n/** The legacy camelCase projection's keys, which no declaration may take either. */\nconst LEGACY_PERMISSION_KEYS: ReadonlySet<string> = new Set<string>(\n ORG_ROLE_PERMISSION_KEYS,\n)\n\n/** The core keys each org role holds when nothing refines it (`org-role-grants.ts`). */\nconst CORE_ROLE_GRANTS: Record<OrgRole, ReadonlySet<string>> = {\n owner: new Set(CORE_ORG_ROLE_GRANTS.owner),\n admin: new Set(CORE_ORG_ROLE_GRANTS.admin),\n editor: new Set(CORE_ORG_ROLE_GRANTS.editor),\n viewer: new Set(CORE_ORG_ROLE_GRANTS.viewer),\n}\n\nconst ORG_ROLES: readonly OrgRole[] = ['owner', 'admin', 'editor', 'viewer']\n\n/**\n * Every permission, in display order for role editors: the core keys, then\n * the keys plugins declare through `registerPluginEntitlements`, in the\n * plugins' catalog order (AGL-2984).\n *\n * LIVE, and kept in step in place. A plugin's declarations register at\n * module scope — at boot on the server, before the first paint in the\n * console — which can be after a module holding this binding was evaluated.\n * So this array, the key list, the role defaults and the empty map below are\n * rewritten in place on every registration instead of being computed once\n * when this module loads, and whoever holds the binding reads the declared\n * keys with the core ones.\n */\nexport const ORG_PERMISSIONS: readonly OrgPermissionDefinition[] = []\n\nexport const ORG_PERMISSION_KEYS: readonly OrgPermission[] = []\n\nconst NO_PERMISSIONS = {} as Record<OrgPermission, boolean>\n\n/**\n * Role → default permission set. Owner/admin hold everything (owner-only\n * actions like org deletion stay role-checked, not permission-checked);\n * editors work on content but see no money or roster controls; viewers read\n * only. A declared key's default for each role is the one its declaration\n * names.\n */\nexport const DEFAULT_ROLE_PERMISSIONS: Record<\n OrgRole,\n Record<OrgPermission, boolean>\n> = {\n owner: {} as Record<OrgPermission, boolean>,\n admin: {} as Record<OrgPermission, boolean>,\n editor: {} as Record<OrgPermission, boolean>,\n viewer: {} as Record<OrgPermission, boolean>,\n}\n\n/** Refused declarations already logged, so a refusal is reported once. */\nconst reportedRefusals = new Set<string>()\n\nfunction replaceItems<T>(target: readonly T[], items: readonly T[]): void {\n const list = target as T[]\n list.length = 0\n list.push(...items)\n}\n\nfunction replaceEntries(\n target: Record<string, boolean>,\n entries: Record<string, boolean>,\n): void {\n for (const key of Object.keys(target)) delete target[key]\n Object.assign(target, entries)\n}\n\n/**\n * Rebuilds the live catalog: the core keys, then every key a plugin declared.\n * A declaration naming a core or legacy key is refused — that key keeps its\n * own meaning and defaults — and logged once.\n */\nfunction syncCatalog(): void {\n const definitions: OrgPermissionDefinition[] = [...CORE_ORG_PERMISSIONS]\n const declaredDefaults = new Map<string, Readonly<Record<OrgRole, boolean>>>()\n for (const declared of listPluginOrgPermissions()) {\n if (\n CORE_ORG_PERMISSION_KEYS.has(declared.key) ||\n LEGACY_PERMISSION_KEYS.has(declared.key)\n ) {\n const refusal = `${declared.pluginId}\\x00${declared.key}`\n if (!reportedRefusals.has(refusal)) {\n reportedRefusals.add(refusal)\n console.error(\n `[plugins] refused catalog permission \"${declared.key}\" to ` +\n `\"${declared.pluginId}\": the key belongs to the core catalog`,\n )\n }\n continue\n }\n definitions.push({\n key: declared.key,\n label: declared.label,\n description: declared.description,\n pluginId: declared.pluginId,\n })\n declaredDefaults.set(declared.key, declared.roleDefaults)\n }\n const keys = definitions.map((definition) => definition.key)\n replaceItems(ORG_PERMISSIONS, definitions)\n replaceItems(ORG_PERMISSION_KEYS, keys)\n replaceEntries(NO_PERMISSIONS, Object.fromEntries(keys.map((key) => [key, false])))\n for (const role of ORG_ROLES) {\n replaceEntries(\n DEFAULT_ROLE_PERMISSIONS[role],\n Object.fromEntries(\n keys.map((key) => [\n key,\n declaredDefaults.has(key)\n ? declaredDefaults.get(key)?.[role] === true\n : CORE_ROLE_GRANTS[role].has(key),\n ]),\n ),\n )\n }\n CATALOG_AND_LEGACY_KEYS.clear()\n for (const key of [...keys, ...LEGACY_PERMISSION_KEYS]) {\n CATALOG_AND_LEGACY_KEYS.add(key)\n }\n}\n\n/** `orgs/{orgId}/roles/{roleId}` — a named custom permission set. */\nexport interface AglynOrgCustomRole {\n $id?: string\n name?: string\n description?: string\n /** Full permission map; missing keys read as false. */\n permissions?: Partial<Record<OrgPermission, boolean>>\n}\n\n/**\n * Effective permission map for a member: the org role's defaults, then\n * the custom role's map (when the member has `roleId` and the role doc\n * is supplied), then per-member overrides — later layers win key-by-key.\n */\nexport function resolveOrgPermissions(\n member:\n | (Partial<AglynOrgMember> & {\n roleId?: string\n permissions?: Partial<Record<OrgPermission, boolean>>\n })\n | null\n | undefined,\n customRole?: AglynOrgCustomRole | null,\n): Record<OrgPermission, boolean> {\n if (!member) return NO_PERMISSIONS\n const role = (member.role ?? 'viewer') as OrgRole\n const base = DEFAULT_ROLE_PERMISSIONS[role] ?? NO_PERMISSIONS\n const merged: Record<OrgPermission, boolean> = { ...base }\n if (member.roleId && customRole?.permissions) {\n for (const key of ORG_PERMISSION_KEYS) {\n const value = customRole.permissions[key]\n if (typeof value === 'boolean') merged[key] = value\n }\n }\n if (member.permissions) {\n for (const key of ORG_PERMISSION_KEYS) {\n const value = member.permissions[key]\n if (typeof value === 'boolean') merged[key] = value\n }\n }\n return merged\n}\n\n/**\n * The values a custom role and a per-member override set EXPLICITLY for\n * permission keys outside this catalog — the keys plugins declare through\n * `registerPluginPermissions` (AGL-2974).\n *\n * `resolveOrgPermissions` walks the catalog alone, so a plugin key stored on\n * a custom role or a member document never reached the `resolvedPermissions`\n * stamp the Firestore rules read. A rule gating on one could then honor\n * neither a grant to an editor nor a revocation from an admin. This carries\n * exactly those stored decisions, custom role first and the per-member\n * override over it, which is the precedence the resolver applies.\n *\n * Tier DEFAULTS are deliberately absent. They live in the plugin registry,\n * which is populated per process by whichever plugin bundles that process\n * loaded, and a stamp computed from it would differ by which route happened\n * to write it. A rule supplies the default itself from the member's role,\n * the way `memberResolves` is always conjoined with a role gate.\n *\n * The legacy six camelCase keys are excluded as well: they are the catalog's\n * older projection, not plugin keys, and no rule reads them.\n */\nexport function explicitPluginPermissionValues(\n member:\n | {\n roleId?: string\n permissions?: Partial<Record<string, unknown>> | null\n }\n | null\n | undefined,\n customRole?: { permissions?: Partial<Record<string, unknown>> | null } | null,\n): Record<string, boolean> {\n const values: Record<string, boolean> = {}\n if (!member) return values\n const layers = [\n member.roleId ? customRole?.permissions : undefined,\n member.permissions,\n ]\n for (const layer of layers) {\n for (const [key, value] of Object.entries(layer ?? {})) {\n if (typeof value !== 'boolean') continue\n if (CATALOG_AND_LEGACY_KEYS.has(key)) continue\n values[key] = value\n }\n }\n return values\n}\n\n/**\n * Keys {@link explicitPluginPermissionValues} leaves to the catalog: every\n * catalog key, declared ones included, and the legacy six. Kept in step by\n * `syncCatalog`.\n */\nconst CATALOG_AND_LEGACY_KEYS = new Set<string>()\n\n/**\n * The `resolvedPermissions` map stamped on a member document for the\n * Firestore rules: the catalog verdict with every layer applied, and beside\n * it the plugin keys a custom role or an override set explicitly.\n *\n * One function for every writer of the stamp, so the org-create transaction\n * and `syncOrgAuthProjections` cannot project different maps. The catalog\n * spreads last; the two key spaces do not overlap, and the order keeps it\n * true that nothing but the resolver decides a catalog key.\n */\nexport function projectMemberResolvedPermissions(\n member: Parameters<typeof resolveOrgPermissions>[0],\n customRole?: AglynOrgCustomRole | null,\n): Record<string, boolean> {\n return {\n ...explicitPluginPermissionValues(member, customRole),\n ...resolveOrgPermissions(member, customRole),\n }\n}\n\n/** Single-permission convenience over `resolveOrgPermissions`. */\nexport function hasOrgPermission(\n member: Parameters<typeof resolveOrgPermissions>[0],\n permission: OrgPermission,\n customRole?: AglynOrgCustomRole | null,\n): boolean {\n return resolveOrgPermissions(member, customRole)[permission]\n}\n\n/** The catalog label for a key — the words a refusal names — or the key itself. */\nexport function orgPermissionLabel(permission: OrgPermission): string {\n return (\n ORG_PERMISSIONS.find((definition) => definition.key === permission)?.label ??\n permission\n )\n}\n\n/** The keys plugins declared into the catalog, in catalog order. */\nexport function pluginOrgPermissionKeys(): OrgPermission[] {\n return ORG_PERMISSIONS.filter((definition) => definition.pluginId).map(\n (definition) => definition.key,\n )\n}\n\n/** One declared catalog key that moved, and the direction it moved in. */\nexport interface PluginPermissionChange {\n permission: string\n granted: boolean\n}\n\n/**\n * The keys PLUGINS declared into the catalog whose value moved between two\n * maps, in the new map's order — what a member, role or collaborator write\n * raises `org.permissions.changed` for, once per key, so the plugin that owns\n * a key can record who moved it.\n *\n * A key the new map leaves unset is not a change: on a role it defers to the\n * base role, on a site toggle to the host role, and neither is a grant or a\n * revocation of its own. A key set for the first time is a change, because\n * the verdict it produces is no longer the default's. The core keys and the\n * legacy six raise nothing; their writes are recorded by the routes' own\n * activity sentences.\n */\nexport function pluginPermissionChanges(\n before: Partial<Record<string, unknown>> | null | undefined,\n after: Partial<Record<string, unknown>> | null | undefined,\n): PluginPermissionChange[] {\n const changes: PluginPermissionChange[] = []\n for (const [key, next] of Object.entries(after ?? {})) {\n if (typeof next !== 'boolean') continue\n if (CORE_ORG_PERMISSION_KEYS.has(key) || LEGACY_PERMISSION_KEYS.has(key)) continue\n if (before?.[key] === next) continue\n changes.push({ permission: key, granted: next })\n }\n return changes\n}\n\n/**\n * The granular (dotted) set projected onto the legacy boolean map\n * (AGL-2350).\n *\n * ## Why this lives here rather than in the console hook\n *\n * It was a private function in `apps/console/hooks/use-org-permissions.ts`,\n * which meant the CLIENT translated the stored permission model into the\n * legacy flags and the SERVER did not translate at all — it derived the\n * legacy flags straight from the built-in role tier and dropped custom roles\n * and per-member overrides on the floor. The two disagreed exactly on the\n * feature `custom-roles.md` sells, in both directions: a permission granted\n * by a custom role showed in the UI and 403'd on POST, and one revoked by an\n * override was hidden in the UI and still succeeded on POST.\n *\n * One exported copy is what stops them drifting again.\n *\n * ## The key spaces are NOT interchangeable, and that is the trap\n *\n * Two permission vocabularies exist. The stored one is DOTTED\n * (`plugins.install`) — `apps/console/app/api/orgs/roles/route.ts` sanitizes\n * against `ORG_PERMISSION_KEYS` before writing, and `AglynOrgMember`\n * `permissions` is typed to it. The legacy one is camelCase\n * (`installPlugins`).\n *\n * `resolveRolePermissions` in `org-roles.ts` accepts `overrides` and\n * `customRoles` arguments keyed by the CAMELCASE space. Feeding it the real\n * stored documents therefore matches no key and changes nothing — it looks\n * like wiring the feature up while doing precisely nothing. Those two\n * parameters are used by nothing but that module's own spec. Translate\n * through here instead.\n *\n * `editHosts` is derived from the ROLE, not from a dotted key: it has no\n * counterpart in the granular catalog and never had one.\n */\nexport function toLegacyPermissions(\n granted: Record<OrgPermission, boolean>,\n role: OrgRole | null | undefined,\n): OrgPermissions {\n return {\n createHosts: granted['hosts.create'],\n editHosts: role !== 'viewer',\n editBilling: granted['billing.manage'],\n publishToMarketplace: granted['marketplace.publish'],\n installPlugins: granted['plugins.install'],\n manageMembers: granted['members.manage'],\n }\n}\n\nsyncCatalog()\nsubscribePluginEntitlements(syncCatalog)\n"],"names":["listPluginOrgPermissions","subscribePluginEntitlements","ORG_ROLE_PERMISSION_KEYS","CORE_ORG_PERMISSION_ENTRIES","CORE_ORG_ROLE_GRANTS","CORE_ORG_PERMISSIONS","map","entry","key","CORE_ORG_PERMISSION_KEYS","Set","definition","LEGACY_PERMISSION_KEYS","CORE_ROLE_GRANTS","owner","admin","editor","viewer","ORG_ROLES","ORG_PERMISSIONS","ORG_PERMISSION_KEYS","NO_PERMISSIONS","DEFAULT_ROLE_PERMISSIONS","reportedRefusals","replaceItems","target","items","list","length","push","replaceEntries","entries","Object","keys","assign","syncCatalog","definitions","declaredDefaults","Map","declared","has","refusal","pluginId","add","console","error","label","description","set","roleDefaults","fromEntries","role","get","CATALOG_AND_LEGACY_KEYS","clear","resolveOrgPermissions","member","customRole","base","merged","roleId","permissions","value","explicitPluginPermissionValues","values","layers","undefined","layer","projectMemberResolvedPermissions","hasOrgPermission","permission","orgPermissionLabel","find","pluginOrgPermissionKeys","filter","pluginPermissionChanges","before","after","changes","next","granted","toLegacyPermissions","createHosts","editHosts","editBilling","publishToMarketplace","installPlugins","manageMembers"],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * WHAT EACH ORG ROLE MAY DO, AS DATA (AGL-243, AGL-3950).
19
+ *
20
+ * The core permission catalog and the core keys each org role holds when
21
+ * nothing refines it. `org-permissions.ts` builds the console's live
22
+ * catalog from these (adding the keys plugins declare, with their own role
23
+ * defaults), and the native apps read the same grants through the generated
24
+ * contracts, so a native gate (`hosts.create`, `data.manage`) answers as the
25
+ * console does for a member row that carries no `resolvedPermissions` stamp.
26
+ *
27
+ * `ORG_ROLE_GRANTS` in the contracts is this table with every plugin's
28
+ * declared catalog keys added (`assembleOrgRoleGrants`, run by
29
+ * `generate:native-contracts` over each plugin's declarations), which is
30
+ * exactly the set the console's `DEFAULT_ROLE_PERMISSIONS` holds once the
31
+ * plugins have registered.
32
+ *
33
+ * NO IMPORTS, on purpose: the native apps read it through the generated
34
+ * contracts.
35
+ */
36
+ /** The four org roles. */
37
+ export type OrgRoleName = 'owner' | 'admin' | 'editor' | 'viewer';
38
+ /** One key of the org permission catalog. */
39
+ export interface OrgPermissionEntry {
40
+ key: string;
41
+ label: string;
42
+ description: string;
43
+ /** The plugin that declared it; absent for a core key. */
44
+ pluginId?: string;
45
+ }
46
+ /** The catalog and each role's default grants. */
47
+ export interface OrgRoleGrants {
48
+ /** Every key, in the order the role editor lists them. */
49
+ permissions: OrgPermissionEntry[];
50
+ /** The keys each role holds when no custom role or per-member override sets them. */
51
+ grants: Record<OrgRoleName, string[]>;
52
+ }
53
+ /** A key a plugin declares into the catalog, as `PluginOrgPermissionDeclaration` carries it. */
54
+ export interface OrgPermissionDeclaration {
55
+ key: string;
56
+ label: string;
57
+ description: string;
58
+ roleDefaults: Record<OrgRoleName, boolean>;
59
+ }
60
+ /** A custom role (`orgs/{orgId}/roles/{roleId}`) or a member's overrides: explicit verdicts per key. */
61
+ export interface OrgPermissionLayer {
62
+ permissions?: Record<string, boolean>;
63
+ }
64
+ export declare const ORG_ROLE_NAMES: readonly OrgRoleName[];
65
+ /**
66
+ * The core keys, in display order for role editors. Every key here is
67
+ * enforced server-side (`apps/console/specs/org-permissions-are-enforced.spec.ts`).
68
+ */
69
+ export declare const CORE_ORG_PERMISSION_ENTRIES: readonly OrgPermissionEntry[];
70
+ /**
71
+ * The core keys each org role holds when nothing refines it: owners and
72
+ * admins hold every core key (owner-only acts like deleting the
73
+ * organization stay role-checked, not permission-checked); editors work on
74
+ * content and see no money or roster controls; viewers read only.
75
+ */
76
+ export declare const CORE_ORG_ROLE_GRANTS: Readonly<Record<OrgRoleName, readonly string[]>>;
77
+ /** The core's catalog and grants; the contracts' value adds the plugins' keys. */
78
+ export declare const ORG_ROLE_GRANTS: OrgRoleGrants;
79
+ /**
80
+ * The catalog with plugins' declared keys after the core ones, each role
81
+ * holding a declared key when its declaration's `roleDefaults` says so. A
82
+ * declaration naming a core key is refused, as the console's catalog refuses
83
+ * it: the core key keeps its meaning and defaults.
84
+ */
85
+ export declare function assembleOrgRoleGrants(core: OrgRoleGrants, declarations: readonly (OrgPermissionDeclaration & {
86
+ pluginId?: string;
87
+ })[]): OrgRoleGrants;
88
+ /**
89
+ * A member's effective permissions, as the console's `resolveOrgPermissions`
90
+ * answers them: the role's defaults, then the custom role's explicit
91
+ * verdicts (when the member has one), then the member's own overrides,
92
+ * later layers winning key by key, over the catalog's keys only. A missing
93
+ * role reads as viewer; a role the catalog does not know holds nothing.
94
+ */
95
+ export declare function rolePermissions(catalog: OrgRoleGrants, role: string | null | undefined, customRole?: OrgPermissionLayer | null, overrides?: OrgPermissionLayer | null): Record<string, boolean>;