@aglyn/aglyn 1.0.0-beta.269 → 1.0.0-beta.271

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 (121) 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/activity-labels.js +2 -0
  6. package/src/lib/app-utils/activity-labels.js.map +1 -1
  7. package/src/lib/app-utils/analytics-events.d.ts +13 -0
  8. package/src/lib/app-utils/analytics-events.js +2 -0
  9. package/src/lib/app-utils/analytics-events.js.map +1 -1
  10. package/src/lib/app-utils/append-only-history.d.ts +94 -0
  11. package/src/lib/app-utils/append-only-history.js +73 -0
  12. package/src/lib/app-utils/append-only-history.js.map +1 -0
  13. package/src/lib/app-utils/bundle-intent.d.ts +186 -0
  14. package/src/lib/app-utils/bundle-intent.js +386 -0
  15. package/src/lib/app-utils/bundle-intent.js.map +1 -0
  16. package/src/lib/app-utils/client-build.d.ts +204 -0
  17. package/src/lib/app-utils/client-build.js +237 -0
  18. package/src/lib/app-utils/client-build.js.map +1 -0
  19. package/src/lib/app-utils/console-routes.d.ts +4 -0
  20. package/src/lib/app-utils/console-routes.js +2 -0
  21. package/src/lib/app-utils/console-routes.js.map +1 -1
  22. package/src/lib/app-utils/country-options.d.ts +27 -0
  23. package/src/lib/app-utils/country-options.js +96 -0
  24. package/src/lib/app-utils/country-options.js.map +1 -0
  25. package/src/lib/app-utils/docs-help-sections.generated.js +7 -5
  26. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  27. package/src/lib/app-utils/docs-help.generated.d.ts +11 -5
  28. package/src/lib/app-utils/docs-help.generated.js +22 -2
  29. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  30. package/src/lib/app-utils/docs-index.generated.js +116 -39
  31. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  32. package/src/lib/app-utils/margin-utilization.js +6 -4
  33. package/src/lib/app-utils/margin-utilization.js.map +1 -1
  34. package/src/lib/app-utils/media-filter.d.ts +26 -0
  35. package/src/lib/app-utils/media-filter.js +32 -0
  36. package/src/lib/app-utils/media-filter.js.map +1 -1
  37. package/src/lib/app-utils/mobile-push.d.ts +8 -0
  38. package/src/lib/app-utils/mobile-push.js +4 -1
  39. package/src/lib/app-utils/mobile-push.js.map +1 -1
  40. package/src/lib/app-utils/native-app-association.d.ts +3 -2
  41. package/src/lib/app-utils/native-app-association.js +4 -2
  42. package/src/lib/app-utils/native-app-association.js.map +1 -1
  43. package/src/lib/app-utils/native-nav.d.ts +265 -0
  44. package/src/lib/app-utils/native-nav.js +741 -0
  45. package/src/lib/app-utils/native-nav.js.map +1 -0
  46. package/src/lib/app-utils/native-themes.d.ts +249 -0
  47. package/src/lib/app-utils/native-themes.js +732 -0
  48. package/src/lib/app-utils/native-themes.js.map +1 -0
  49. package/src/lib/app-utils/notifications.d.ts +1 -1
  50. package/src/lib/app-utils/notifications.js +10 -0
  51. package/src/lib/app-utils/notifications.js.map +1 -1
  52. package/src/lib/app-utils/onboarding-deep-link.d.ts +36 -2
  53. package/src/lib/app-utils/onboarding-deep-link.js +48 -5
  54. package/src/lib/app-utils/onboarding-deep-link.js.map +1 -1
  55. package/src/lib/app-utils/one-box-search.d.ts +108 -0
  56. package/src/lib/app-utils/one-box-search.js +167 -0
  57. package/src/lib/app-utils/one-box-search.js.map +1 -0
  58. package/src/lib/app-utils/operator-alerts.js +21 -0
  59. package/src/lib/app-utils/operator-alerts.js.map +1 -1
  60. package/src/lib/app-utils/org-permissions.js +10 -61
  61. package/src/lib/app-utils/org-permissions.js.map +1 -1
  62. package/src/lib/app-utils/org-role-grants.d.ts +95 -0
  63. package/src/lib/app-utils/org-role-grants.js +193 -0
  64. package/src/lib/app-utils/org-role-grants.js.map +1 -0
  65. package/src/lib/app-utils/owner-approval-record.d.ts +102 -0
  66. package/src/lib/app-utils/owner-approval-record.js +148 -0
  67. package/src/lib/app-utils/owner-approval-record.js.map +1 -0
  68. package/src/lib/app-utils/plan-entitlements.d.ts +224 -68
  69. package/src/lib/app-utils/plan-entitlements.js +278 -61
  70. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  71. package/src/lib/app-utils/release-flags.d.ts +1 -1
  72. package/src/lib/app-utils/release-flags.js +10 -0
  73. package/src/lib/app-utils/release-flags.js.map +1 -1
  74. package/src/lib/app-utils/server.d.ts +4 -0
  75. package/src/lib/app-utils/server.js +7 -0
  76. package/src/lib/app-utils/server.js.map +1 -1
  77. package/src/lib/app-utils/site-care-project.d.ts +227 -0
  78. package/src/lib/app-utils/site-care-project.js +423 -0
  79. package/src/lib/app-utils/site-care-project.js.map +1 -0
  80. package/src/lib/app-utils/site-care.d.ts +268 -0
  81. package/src/lib/app-utils/site-care.js +283 -0
  82. package/src/lib/app-utils/site-care.js.map +1 -0
  83. package/src/lib/app-utils/tenant-dns-targets.d.ts +62 -0
  84. package/src/lib/app-utils/tenant-dns-targets.js +80 -0
  85. package/src/lib/app-utils/tenant-dns-targets.js.map +1 -0
  86. package/src/lib/app-utils/upgrade-proposal.d.ts +29 -4
  87. package/src/lib/app-utils/upgrade-proposal.js +60 -9
  88. package/src/lib/app-utils/upgrade-proposal.js.map +1 -1
  89. package/src/lib/foundation/definitions/org-billing.types.d.ts +54 -10
  90. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  91. package/src/lib/foundation/definitions/organization.types.d.ts +3 -1
  92. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  93. package/src/lib/foundation/definitions/platform.types.d.ts +9 -0
  94. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  95. package/src/lib/foundation/index.d.ts +1 -1
  96. package/src/lib/foundation/index.js.map +1 -1
  97. package/src/lib/plugin-manager/feature-plugins.d.ts +86 -0
  98. package/src/lib/plugin-manager/feature-plugins.js +20 -0
  99. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  100. package/src/lib/plugin-manager/first-party-plugins.generated.js +22 -1
  101. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  102. package/src/lib/plugin-manager/plugin-ai-capabilities.d.ts +19 -0
  103. package/src/lib/plugin-manager/plugin-ai-capabilities.js.map +1 -1
  104. package/src/lib/plugin-manager/plugin-domain-events.d.ts +4 -3
  105. package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -1
  106. package/src/lib/plugin-manager/plugin-domain-search.d.ts +81 -0
  107. package/src/lib/plugin-manager/plugin-domain-search.js +63 -0
  108. package/src/lib/plugin-manager/plugin-domain-search.js.map +1 -0
  109. package/src/lib/plugin-manager/plugin-entitlements.d.ts +23 -5
  110. package/src/lib/plugin-manager/plugin-entitlements.js +10 -0
  111. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  112. package/src/lib/plugin-manager/plugin-media-ingest.d.ts +25 -0
  113. package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -1
  114. package/src/lib/plugin-manager/plugin-search-sources.d.ts +85 -0
  115. package/src/lib/plugin-manager/plugin-search-sources.js +91 -0
  116. package/src/lib/plugin-manager/plugin-search-sources.js.map +1 -0
  117. package/src/lib/plugin-manager/plugin-services.js +6 -0
  118. package/src/lib/plugin-manager/plugin-services.js.map +1 -1
  119. package/src/lib/plugin-manager/purchase-gate-preclearance.d.ts +61 -0
  120. package/src/lib/plugin-manager/purchase-gate-preclearance.js +102 -0
  121. package/src/lib/plugin-manager/purchase-gate-preclearance.js.map +1 -0
@@ -0,0 +1,193 @@
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
+ * WHAT EACH ORG ROLE MAY DO, AS DATA (AGL-243, AGL-3950).
18
+ *
19
+ * The core permission catalog and the core keys each org role holds when
20
+ * nothing refines it. `org-permissions.ts` builds the console's live
21
+ * catalog from these (adding the keys plugins declare, with their own role
22
+ * defaults), and the native apps read the same grants through the generated
23
+ * contracts, so a native gate (`hosts.create`, `data.manage`) answers as the
24
+ * console does for a member row that carries no `resolvedPermissions` stamp.
25
+ *
26
+ * `ORG_ROLE_GRANTS` in the contracts is this table with every plugin's
27
+ * declared catalog keys added (`assembleOrgRoleGrants`, run by
28
+ * `generate:native-contracts` over each plugin's declarations), which is
29
+ * exactly the set the console's `DEFAULT_ROLE_PERMISSIONS` holds once the
30
+ * plugins have registered.
31
+ *
32
+ * NO IMPORTS, on purpose: the native apps read it through the generated
33
+ * contracts.
34
+ */ /** The four org roles. */ import { _ as _extends } from "@swc/helpers/_/_extends";
35
+ export const ORG_ROLE_NAMES = [
36
+ 'owner',
37
+ 'admin',
38
+ 'editor',
39
+ 'viewer'
40
+ ];
41
+ /**
42
+ * The core keys, in display order for role editors. Every key here is
43
+ * enforced server-side (`apps/console/specs/org-permissions-are-enforced.spec.ts`).
44
+ */ export const CORE_ORG_PERMISSION_ENTRIES = [
45
+ {
46
+ key: 'org.settings',
47
+ label: 'Organization settings',
48
+ description: 'Rename the organization, change its slug, transfer it.'
49
+ },
50
+ {
51
+ key: 'org.auditLog',
52
+ label: 'Activity & audit log',
53
+ description: 'See the organization activity feed.'
54
+ },
55
+ {
56
+ key: 'billing.view',
57
+ label: 'View billing',
58
+ description: 'See the plan, usage meters, and invoices.'
59
+ },
60
+ {
61
+ key: 'billing.manage',
62
+ label: 'Manage billing',
63
+ description: 'Change plans, buy add-ons, update payment details.'
64
+ },
65
+ {
66
+ key: 'members.manage',
67
+ label: 'Manage members',
68
+ description: 'Invite, remove, and re-role organization members.'
69
+ },
70
+ {
71
+ key: 'hosts.create',
72
+ label: 'Create sites',
73
+ description: 'Create new sites in the organization.'
74
+ },
75
+ {
76
+ key: 'hosts.delete',
77
+ label: 'Delete sites',
78
+ description: 'Delete sites the member can access.'
79
+ },
80
+ {
81
+ key: 'data.manage',
82
+ label: 'Manage data',
83
+ description: 'Create, edit, and delete organization datasets.'
84
+ },
85
+ {
86
+ key: 'marketplace.publish',
87
+ label: 'Publish to marketplace',
88
+ description: 'Publish listings under the organization profile.'
89
+ },
90
+ {
91
+ key: 'plugins.install',
92
+ label: 'Install plugins',
93
+ description: 'Install or remove marketplace plugins.'
94
+ }
95
+ ];
96
+ const CORE_KEYS = CORE_ORG_PERMISSION_ENTRIES.map((entry)=>entry.key);
97
+ /**
98
+ * The core keys each org role holds when nothing refines it: owners and
99
+ * admins hold every core key (owner-only acts like deleting the
100
+ * organization stay role-checked, not permission-checked); editors work on
101
+ * content and see no money or roster controls; viewers read only.
102
+ */ export const CORE_ORG_ROLE_GRANTS = {
103
+ owner: CORE_KEYS,
104
+ admin: CORE_KEYS,
105
+ editor: [
106
+ 'data.manage',
107
+ 'marketplace.publish',
108
+ 'plugins.install'
109
+ ],
110
+ viewer: []
111
+ };
112
+ /** The core's catalog and grants; the contracts' value adds the plugins' keys. */ export const ORG_ROLE_GRANTS = {
113
+ permissions: CORE_ORG_PERMISSION_ENTRIES.map((entry)=>_extends({}, entry)),
114
+ grants: {
115
+ owner: [
116
+ ...CORE_ORG_ROLE_GRANTS.owner
117
+ ],
118
+ admin: [
119
+ ...CORE_ORG_ROLE_GRANTS.admin
120
+ ],
121
+ editor: [
122
+ ...CORE_ORG_ROLE_GRANTS.editor
123
+ ],
124
+ viewer: [
125
+ ...CORE_ORG_ROLE_GRANTS.viewer
126
+ ]
127
+ }
128
+ };
129
+ /**
130
+ * The catalog with plugins' declared keys after the core ones, each role
131
+ * holding a declared key when its declaration's `roleDefaults` says so. A
132
+ * declaration naming a core key is refused, as the console's catalog refuses
133
+ * it: the core key keeps its meaning and defaults.
134
+ */ export function assembleOrgRoleGrants(core, declarations) {
135
+ const permissions = [
136
+ ...core.permissions
137
+ ];
138
+ const grants = Object.fromEntries(ORG_ROLE_NAMES.map((role)=>[
139
+ role,
140
+ [
141
+ ...core.grants[role]
142
+ ]
143
+ ]));
144
+ const taken = new Set(permissions.map((entry)=>entry.key));
145
+ for (const declaration of declarations){
146
+ if (!(declaration == null ? void 0 : declaration.key) || taken.has(declaration.key)) continue;
147
+ taken.add(declaration.key);
148
+ permissions.push(_extends({
149
+ key: declaration.key,
150
+ label: declaration.label,
151
+ description: declaration.description
152
+ }, declaration.pluginId ? {
153
+ pluginId: declaration.pluginId
154
+ } : {}));
155
+ for (const role of ORG_ROLE_NAMES){
156
+ var _declaration_roleDefaults;
157
+ if (((_declaration_roleDefaults = declaration.roleDefaults) == null ? void 0 : _declaration_roleDefaults[role]) === true) grants[role].push(declaration.key);
158
+ }
159
+ }
160
+ return {
161
+ permissions,
162
+ grants
163
+ };
164
+ }
165
+ /**
166
+ * A member's effective permissions, as the console's `resolveOrgPermissions`
167
+ * answers them: the role's defaults, then the custom role's explicit
168
+ * verdicts (when the member has one), then the member's own overrides,
169
+ * later layers winning key by key, over the catalog's keys only. A missing
170
+ * role reads as viewer; a role the catalog does not know holds nothing.
171
+ */ export function rolePermissions(catalog, role, customRole, overrides) {
172
+ var _catalog_grants_name;
173
+ const keys = catalog.permissions.map((entry)=>entry.key);
174
+ const name = role != null ? role : 'viewer';
175
+ const held = new Set((_catalog_grants_name = catalog.grants[name]) != null ? _catalog_grants_name : []);
176
+ const out = Object.fromEntries(keys.map((key)=>[
177
+ key,
178
+ held.has(key)
179
+ ]));
180
+ for (const layer of [
181
+ customRole,
182
+ overrides
183
+ ]){
184
+ const values = layer == null ? void 0 : layer.permissions;
185
+ if (!values) continue;
186
+ for (const key of keys){
187
+ if (typeof values[key] === 'boolean') out[key] = values[key];
188
+ }
189
+ }
190
+ return out;
191
+ }
192
+
193
+ //# sourceMappingURL=org-role-grants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/org-role-grants.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 * WHAT EACH ORG ROLE MAY DO, AS DATA (AGL-243, AGL-3950).\n *\n * The core permission catalog and the core keys each org role holds when\n * nothing refines it. `org-permissions.ts` builds the console's live\n * catalog from these (adding the keys plugins declare, with their own role\n * defaults), and the native apps read the same grants through the generated\n * contracts, so a native gate (`hosts.create`, `data.manage`) answers as the\n * console does for a member row that carries no `resolvedPermissions` stamp.\n *\n * `ORG_ROLE_GRANTS` in the contracts is this table with every plugin's\n * declared catalog keys added (`assembleOrgRoleGrants`, run by\n * `generate:native-contracts` over each plugin's declarations), which is\n * exactly the set the console's `DEFAULT_ROLE_PERMISSIONS` holds once the\n * plugins have registered.\n *\n * NO IMPORTS, on purpose: the native apps read it through the generated\n * contracts.\n */\n\n/** The four org roles. */\nexport type OrgRoleName = 'owner' | 'admin' | 'editor' | 'viewer'\n\n/** One key of the org permission catalog. */\nexport interface OrgPermissionEntry {\n key: string\n label: string\n description: string\n /** The plugin that declared it; absent for a core key. */\n pluginId?: string\n}\n\n/** The catalog and each role's default grants. */\nexport interface OrgRoleGrants {\n /** Every key, in the order the role editor lists them. */\n permissions: OrgPermissionEntry[]\n /** The keys each role holds when no custom role or per-member override sets them. */\n grants: Record<OrgRoleName, string[]>\n}\n\n/** A key a plugin declares into the catalog, as `PluginOrgPermissionDeclaration` carries it. */\nexport interface OrgPermissionDeclaration {\n key: string\n label: string\n description: string\n roleDefaults: Record<OrgRoleName, boolean>\n}\n\n/** A custom role (`orgs/{orgId}/roles/{roleId}`) or a member's overrides: explicit verdicts per key. */\nexport interface OrgPermissionLayer {\n permissions?: Record<string, boolean>\n}\n\nexport const ORG_ROLE_NAMES: readonly OrgRoleName[] = ['owner', 'admin', 'editor', 'viewer']\n\n/**\n * The core keys, in display order for role editors. Every key here is\n * enforced server-side (`apps/console/specs/org-permissions-are-enforced.spec.ts`).\n */\nexport const CORE_ORG_PERMISSION_ENTRIES: readonly OrgPermissionEntry[] = [\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\nconst CORE_KEYS = CORE_ORG_PERMISSION_ENTRIES.map((entry) => entry.key)\n\n/**\n * The core keys each org role holds when nothing refines it: owners and\n * admins hold every core key (owner-only acts like deleting the\n * organization stay role-checked, not permission-checked); editors work on\n * content and see no money or roster controls; viewers read only.\n */\nexport const CORE_ORG_ROLE_GRANTS: Readonly<Record<OrgRoleName, readonly string[]>> = {\n owner: CORE_KEYS,\n admin: CORE_KEYS,\n editor: ['data.manage', 'marketplace.publish', 'plugins.install'],\n viewer: [],\n}\n\n/** The core's catalog and grants; the contracts' value adds the plugins' keys. */\nexport const ORG_ROLE_GRANTS: OrgRoleGrants = {\n permissions: CORE_ORG_PERMISSION_ENTRIES.map((entry) => ({ ...entry })),\n grants: {\n owner: [...CORE_ORG_ROLE_GRANTS.owner],\n admin: [...CORE_ORG_ROLE_GRANTS.admin],\n editor: [...CORE_ORG_ROLE_GRANTS.editor],\n viewer: [...CORE_ORG_ROLE_GRANTS.viewer],\n },\n}\n\n/**\n * The catalog with plugins' declared keys after the core ones, each role\n * holding a declared key when its declaration's `roleDefaults` says so. A\n * declaration naming a core key is refused, as the console's catalog refuses\n * it: the core key keeps its meaning and defaults.\n */\nexport function assembleOrgRoleGrants(\n core: OrgRoleGrants,\n declarations: readonly (OrgPermissionDeclaration & { pluginId?: string })[],\n): OrgRoleGrants {\n const permissions = [...core.permissions]\n const grants = Object.fromEntries(ORG_ROLE_NAMES.map((role) => [role, [...core.grants[role]]])) as Record<\n OrgRoleName,\n string[]\n >\n const taken = new Set(permissions.map((entry) => entry.key))\n for (const declaration of declarations) {\n if (!declaration?.key || taken.has(declaration.key)) continue\n taken.add(declaration.key)\n permissions.push({\n key: declaration.key,\n label: declaration.label,\n description: declaration.description,\n ...(declaration.pluginId ? { pluginId: declaration.pluginId } : {}),\n })\n for (const role of ORG_ROLE_NAMES) {\n if (declaration.roleDefaults?.[role] === true) grants[role].push(declaration.key)\n }\n }\n return { permissions, grants }\n}\n\n/**\n * A member's effective permissions, as the console's `resolveOrgPermissions`\n * answers them: the role's defaults, then the custom role's explicit\n * verdicts (when the member has one), then the member's own overrides,\n * later layers winning key by key, over the catalog's keys only. A missing\n * role reads as viewer; a role the catalog does not know holds nothing.\n */\nexport function rolePermissions(\n catalog: OrgRoleGrants,\n role: string | null | undefined,\n customRole?: OrgPermissionLayer | null,\n overrides?: OrgPermissionLayer | null,\n): Record<string, boolean> {\n const keys = catalog.permissions.map((entry) => entry.key)\n const name = role ?? 'viewer'\n const held = new Set((catalog.grants as Record<string, string[] | undefined>)[name] ?? [])\n const out: Record<string, boolean> = Object.fromEntries(keys.map((key) => [key, held.has(key)]))\n for (const layer of [customRole, overrides]) {\n const values = layer?.permissions\n if (!values) continue\n for (const key of keys) {\n if (typeof values[key] === 'boolean') out[key] = values[key]\n }\n }\n return out\n}\n"],"names":["ORG_ROLE_NAMES","CORE_ORG_PERMISSION_ENTRIES","key","label","description","CORE_KEYS","map","entry","CORE_ORG_ROLE_GRANTS","owner","admin","editor","viewer","ORG_ROLE_GRANTS","permissions","grants","assembleOrgRoleGrants","core","declarations","Object","fromEntries","role","taken","Set","declaration","has","add","push","pluginId","roleDefaults","rolePermissions","catalog","customRole","overrides","keys","name","held","out","layer","values"],"mappings":"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"}
@@ -0,0 +1,102 @@
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
+ * THE OWNER'S RECORDED YES (AGL-3959).
19
+ *
20
+ * Paid work beyond what a subscription includes is billed only after the
21
+ * owner agreed to it, and the owner never clicks anything in the console to
22
+ * agree: they say yes to a person — at the booth, on the phone, by text or by
23
+ * email — and STAFF record how and when. Saving the record emails the owner a
24
+ * confirmation, and that email is the written record.
25
+ *
26
+ * This module is the record's shape and its rules, with no Firestore and no
27
+ * product in it, so any staff surface that bills by an approval reads it the
28
+ * same way: Site Care's extra time first, and the flat-quoted project invoice
29
+ * (AGL-3970) after it. A correction never edits a record in place: it is a
30
+ * new record with `version` one higher, `corrected: true` and the reason, and
31
+ * the old one stays in the request's history.
32
+ */
33
+ /** How the owner said yes. */
34
+ export declare const OWNER_APPROVAL_CHANNELS: readonly ["in-person", "phone", "text", "email"];
35
+ export type OwnerApprovalChannel = (typeof OWNER_APPROVAL_CHANNELS)[number];
36
+ /** The channel as a sentence reads it: "by phone", "in person". */
37
+ export declare const OWNER_APPROVAL_CHANNEL_WORDS: Readonly<Record<OwnerApprovalChannel, string>>;
38
+ /** The channel as a picker labels it. */
39
+ export declare const OWNER_APPROVAL_CHANNEL_LABELS: Readonly<Record<OwnerApprovalChannel, string>>;
40
+ /** The longest approval an hourly record may carry, in hours. */
41
+ export declare const OWNER_APPROVAL_MAX_HOURS = 40;
42
+ /** The longest note or reason a record keeps. */
43
+ export declare const OWNER_APPROVAL_NOTE_MAX = 500;
44
+ /** One recorded yes. Written by staff, read by the close and the emails. */
45
+ export interface OwnerApprovalRecord {
46
+ /** Whole hours the owner agreed to, at most. */
47
+ hours: number;
48
+ /** The published hourly rate the owner agreed to, in whole dollars. */
49
+ rateUsd: number;
50
+ channel: OwnerApprovalChannel;
51
+ /** When the owner said yes, epoch ms. */
52
+ approvedAtMs: number;
53
+ /** When staff recorded it, epoch ms. */
54
+ recordedAtMs: number;
55
+ recordedByUid: string;
56
+ recordedByEmail: string | null;
57
+ /** What the owner said yes to, in staff's words; optional. */
58
+ note: string | null;
59
+ /** 1 for the first record; each correction is one higher. */
60
+ version: number;
61
+ /** True on every record after the first. */
62
+ corrected: boolean;
63
+ /** Why it was corrected; required when `corrected`. */
64
+ correctionReason: string | null;
65
+ }
66
+ export type OwnerApprovalInputResult = {
67
+ ok: true;
68
+ record: OwnerApprovalRecord;
69
+ } | {
70
+ ok: false;
71
+ error: string;
72
+ };
73
+ /**
74
+ * Reads what staff typed into a record, refusing anything the record cannot
75
+ * honestly say. `previous` is the record it corrects, when there is one: a
76
+ * correction needs a reason, and the reason is what the history and the
77
+ * re-sent confirmation say changed.
78
+ */
79
+ export declare function readOwnerApprovalInput(raw: Readonly<Record<string, unknown>> | null | undefined, context: {
80
+ rateUsd: number;
81
+ nowMs: number;
82
+ by: {
83
+ uid: string;
84
+ email: string | null;
85
+ };
86
+ previous?: OwnerApprovalRecord | null;
87
+ /**
88
+ * The most hours the record may carry; `OWNER_APPROVAL_MAX_HOURS` unless
89
+ * the surface says otherwise. A flat-quoted project (AGL-3970) records
90
+ * the quote's own whole hours, which a redesign can take past a week.
91
+ */
92
+ maxHours?: number;
93
+ }): OwnerApprovalInputResult;
94
+ /** A stored record read back defensively, or null when it is not one. */
95
+ export declare function readOwnerApprovalRecord(value: unknown): OwnerApprovalRecord | null;
96
+ /** "2 hours", "1 hour". */
97
+ export declare function describeHours(hours: number): string;
98
+ /**
99
+ * The confirmation's one sentence, in the owner's words: what they agreed
100
+ * to, how and when.
101
+ */
102
+ export declare function ownerApprovalSentence(record: Pick<OwnerApprovalRecord, 'hours' | 'rateUsd' | 'channel' | 'approvedAtMs'>, what: string, formatDate: (ms: number) => string): string;
@@ -0,0 +1,148 @@
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
+ * THE OWNER'S RECORDED YES (AGL-3959).
18
+ *
19
+ * Paid work beyond what a subscription includes is billed only after the
20
+ * owner agreed to it, and the owner never clicks anything in the console to
21
+ * agree: they say yes to a person — at the booth, on the phone, by text or by
22
+ * email — and STAFF record how and when. Saving the record emails the owner a
23
+ * confirmation, and that email is the written record.
24
+ *
25
+ * This module is the record's shape and its rules, with no Firestore and no
26
+ * product in it, so any staff surface that bills by an approval reads it the
27
+ * same way: Site Care's extra time first, and the flat-quoted project invoice
28
+ * (AGL-3970) after it. A correction never edits a record in place: it is a
29
+ * new record with `version` one higher, `corrected: true` and the reason, and
30
+ * the old one stays in the request's history.
31
+ */ /** How the owner said yes. */ export const OWNER_APPROVAL_CHANNELS = [
32
+ 'in-person',
33
+ 'phone',
34
+ 'text',
35
+ 'email'
36
+ ];
37
+ /** The channel as a sentence reads it: "by phone", "in person". */ export const OWNER_APPROVAL_CHANNEL_WORDS = {
38
+ 'in-person': 'in person',
39
+ phone: 'by phone',
40
+ text: 'by text message',
41
+ email: 'by email'
42
+ };
43
+ /** The channel as a picker labels it. */ export const OWNER_APPROVAL_CHANNEL_LABELS = {
44
+ 'in-person': 'In person',
45
+ phone: 'Phone call',
46
+ text: 'Text message',
47
+ email: 'Email'
48
+ };
49
+ /** The longest approval an hourly record may carry, in hours. */ export const OWNER_APPROVAL_MAX_HOURS = 40;
50
+ /** The longest note or reason a record keeps. */ export const OWNER_APPROVAL_NOTE_MAX = 500;
51
+ /** A trimmed, bounded string, or null when blank. */ function cleanText(value, max) {
52
+ const text = typeof value === 'string' ? value.trim().slice(0, max) : '';
53
+ return text ? text : null;
54
+ }
55
+ /**
56
+ * Reads what staff typed into a record, refusing anything the record cannot
57
+ * honestly say. `previous` is the record it corrects, when there is one: a
58
+ * correction needs a reason, and the reason is what the history and the
59
+ * re-sent confirmation say changed.
60
+ */ export function readOwnerApprovalInput(raw, context) {
61
+ var _context_maxHours, _ref, _context_previous;
62
+ const hours = Number(raw == null ? void 0 : raw['hours']);
63
+ const maxHours = (_context_maxHours = context.maxHours) != null ? _context_maxHours : OWNER_APPROVAL_MAX_HOURS;
64
+ if (!Number.isInteger(hours) || hours < 1 || hours > maxHours) {
65
+ return {
66
+ ok: false,
67
+ error: `Enter the hours the owner approved, a whole number from 1 to ${maxHours}.`
68
+ };
69
+ }
70
+ const channel = String((_ref = raw == null ? void 0 : raw['channel']) != null ? _ref : '');
71
+ if (!OWNER_APPROVAL_CHANNELS.includes(channel)) {
72
+ return {
73
+ ok: false,
74
+ error: 'Choose how the owner said yes.'
75
+ };
76
+ }
77
+ const approvedAtMs = Number(raw == null ? void 0 : raw['approvedAtMs']);
78
+ if (!Number.isFinite(approvedAtMs) || approvedAtMs <= 0) {
79
+ return {
80
+ ok: false,
81
+ error: 'Enter when the owner said yes.'
82
+ };
83
+ }
84
+ // A minute of clock skew is allowed; a yes recorded for tomorrow is not one
85
+ // anybody has heard yet.
86
+ if (approvedAtMs > context.nowMs + 60000) {
87
+ return {
88
+ ok: false,
89
+ error: 'The owner’s yes cannot be in the future.'
90
+ };
91
+ }
92
+ const previous = (_context_previous = context.previous) != null ? _context_previous : null;
93
+ const correctionReason = cleanText(raw == null ? void 0 : raw['reason'], OWNER_APPROVAL_NOTE_MAX);
94
+ if (previous && !correctionReason) {
95
+ return {
96
+ ok: false,
97
+ error: 'Say why the approval is being corrected.'
98
+ };
99
+ }
100
+ return {
101
+ ok: true,
102
+ record: {
103
+ hours,
104
+ rateUsd: context.rateUsd,
105
+ channel,
106
+ approvedAtMs: Math.floor(approvedAtMs),
107
+ recordedAtMs: context.nowMs,
108
+ recordedByUid: context.by.uid,
109
+ recordedByEmail: context.by.email,
110
+ note: cleanText(raw == null ? void 0 : raw['note'], OWNER_APPROVAL_NOTE_MAX),
111
+ version: previous ? Math.max(1, Number(previous.version) || 1) + 1 : 1,
112
+ corrected: Boolean(previous),
113
+ correctionReason: previous ? correctionReason : null
114
+ }
115
+ };
116
+ }
117
+ /** A stored record read back defensively, or null when it is not one. */ export function readOwnerApprovalRecord(value) {
118
+ var _record_recordedByUid, _record_recordedByEmail, _record_note, _record_correctionReason;
119
+ if (!value || typeof value !== 'object') return null;
120
+ const record = value;
121
+ const hours = Number(record.hours);
122
+ if (!Number.isInteger(hours) || hours < 1) return null;
123
+ if (!OWNER_APPROVAL_CHANNELS.includes(record.channel)) return null;
124
+ return {
125
+ hours,
126
+ rateUsd: Number(record.rateUsd) || 0,
127
+ channel: record.channel,
128
+ approvedAtMs: Number(record.approvedAtMs) || 0,
129
+ recordedAtMs: Number(record.recordedAtMs) || 0,
130
+ recordedByUid: String((_record_recordedByUid = record.recordedByUid) != null ? _record_recordedByUid : ''),
131
+ recordedByEmail: (_record_recordedByEmail = record.recordedByEmail) != null ? _record_recordedByEmail : null,
132
+ note: (_record_note = record.note) != null ? _record_note : null,
133
+ version: Number(record.version) || 1,
134
+ corrected: record.corrected === true,
135
+ correctionReason: (_record_correctionReason = record.correctionReason) != null ? _record_correctionReason : null
136
+ };
137
+ }
138
+ /** "2 hours", "1 hour". */ export function describeHours(hours) {
139
+ return `${hours} ${hours === 1 ? 'hour' : 'hours'}`;
140
+ }
141
+ /**
142
+ * The confirmation's one sentence, in the owner's words: what they agreed
143
+ * to, how and when.
144
+ */ export function ownerApprovalSentence(record, what, formatDate) {
145
+ return `You approved up to ${describeHours(record.hours)} of extra time at ` + `$${record.rateUsd} an hour for “${what}”, ` + `${OWNER_APPROVAL_CHANNEL_WORDS[record.channel]} on ${formatDate(record.approvedAtMs)}.`;
146
+ }
147
+
148
+ //# sourceMappingURL=owner-approval-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/owner-approval-record.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 OWNER'S RECORDED YES (AGL-3959).\n *\n * Paid work beyond what a subscription includes is billed only after the\n * owner agreed to it, and the owner never clicks anything in the console to\n * agree: they say yes to a person — at the booth, on the phone, by text or by\n * email — and STAFF record how and when. Saving the record emails the owner a\n * confirmation, and that email is the written record.\n *\n * This module is the record's shape and its rules, with no Firestore and no\n * product in it, so any staff surface that bills by an approval reads it the\n * same way: Site Care's extra time first, and the flat-quoted project invoice\n * (AGL-3970) after it. A correction never edits a record in place: it is a\n * new record with `version` one higher, `corrected: true` and the reason, and\n * the old one stays in the request's history.\n */\n\n/** How the owner said yes. */\nexport const OWNER_APPROVAL_CHANNELS = ['in-person', 'phone', 'text', 'email'] as const\n\nexport type OwnerApprovalChannel = (typeof OWNER_APPROVAL_CHANNELS)[number]\n\n/** The channel as a sentence reads it: \"by phone\", \"in person\". */\nexport const OWNER_APPROVAL_CHANNEL_WORDS: Readonly<Record<OwnerApprovalChannel, string>> = {\n 'in-person': 'in person',\n phone: 'by phone',\n text: 'by text message',\n email: 'by email',\n}\n\n/** The channel as a picker labels it. */\nexport const OWNER_APPROVAL_CHANNEL_LABELS: Readonly<Record<OwnerApprovalChannel, string>> = {\n 'in-person': 'In person',\n phone: 'Phone call',\n text: 'Text message',\n email: 'Email',\n}\n\n/** The longest approval an hourly record may carry, in hours. */\nexport const OWNER_APPROVAL_MAX_HOURS = 40\n\n/** The longest note or reason a record keeps. */\nexport const OWNER_APPROVAL_NOTE_MAX = 500\n\n/** One recorded yes. Written by staff, read by the close and the emails. */\nexport interface OwnerApprovalRecord {\n /** Whole hours the owner agreed to, at most. */\n hours: number\n /** The published hourly rate the owner agreed to, in whole dollars. */\n rateUsd: number\n channel: OwnerApprovalChannel\n /** When the owner said yes, epoch ms. */\n approvedAtMs: number\n /** When staff recorded it, epoch ms. */\n recordedAtMs: number\n recordedByUid: string\n recordedByEmail: string | null\n /** What the owner said yes to, in staff's words; optional. */\n note: string | null\n /** 1 for the first record; each correction is one higher. */\n version: number\n /** True on every record after the first. */\n corrected: boolean\n /** Why it was corrected; required when `corrected`. */\n correctionReason: string | null\n}\n\nexport type OwnerApprovalInputResult =\n | { ok: true; record: OwnerApprovalRecord }\n | { ok: false; error: string }\n\n/** A trimmed, bounded string, or null when blank. */\nfunction cleanText(value: unknown, max: number): string | null {\n const text = typeof value === 'string' ? value.trim().slice(0, max) : ''\n return text ? text : null\n}\n\n/**\n * Reads what staff typed into a record, refusing anything the record cannot\n * honestly say. `previous` is the record it corrects, when there is one: a\n * correction needs a reason, and the reason is what the history and the\n * re-sent confirmation say changed.\n */\nexport function readOwnerApprovalInput(\n raw: Readonly<Record<string, unknown>> | null | undefined,\n context: {\n rateUsd: number\n nowMs: number\n by: { uid: string; email: string | null }\n previous?: OwnerApprovalRecord | null\n /**\n * The most hours the record may carry; `OWNER_APPROVAL_MAX_HOURS` unless\n * the surface says otherwise. A flat-quoted project (AGL-3970) records\n * the quote's own whole hours, which a redesign can take past a week.\n */\n maxHours?: number\n },\n): OwnerApprovalInputResult {\n const hours = Number(raw?.['hours'])\n const maxHours = context.maxHours ?? OWNER_APPROVAL_MAX_HOURS\n if (!Number.isInteger(hours) || hours < 1 || hours > maxHours) {\n return {\n ok: false,\n error: `Enter the hours the owner approved, a whole number from 1 to ${maxHours}.`,\n }\n }\n const channel = String(raw?.['channel'] ?? '') as OwnerApprovalChannel\n if (!OWNER_APPROVAL_CHANNELS.includes(channel)) {\n return { ok: false, error: 'Choose how the owner said yes.' }\n }\n const approvedAtMs = Number(raw?.['approvedAtMs'])\n if (!Number.isFinite(approvedAtMs) || approvedAtMs <= 0) {\n return { ok: false, error: 'Enter when the owner said yes.' }\n }\n // A minute of clock skew is allowed; a yes recorded for tomorrow is not one\n // anybody has heard yet.\n if (approvedAtMs > context.nowMs + 60_000) {\n return { ok: false, error: 'The owner’s yes cannot be in the future.' }\n }\n const previous = context.previous ?? null\n const correctionReason = cleanText(raw?.['reason'], OWNER_APPROVAL_NOTE_MAX)\n if (previous && !correctionReason) {\n return { ok: false, error: 'Say why the approval is being corrected.' }\n }\n return {\n ok: true,\n record: {\n hours,\n rateUsd: context.rateUsd,\n channel,\n approvedAtMs: Math.floor(approvedAtMs),\n recordedAtMs: context.nowMs,\n recordedByUid: context.by.uid,\n recordedByEmail: context.by.email,\n note: cleanText(raw?.['note'], OWNER_APPROVAL_NOTE_MAX),\n version: previous ? Math.max(1, Number(previous.version) || 1) + 1 : 1,\n corrected: Boolean(previous),\n correctionReason: previous ? correctionReason : null,\n },\n }\n}\n\n/** A stored record read back defensively, or null when it is not one. */\nexport function readOwnerApprovalRecord(value: unknown): OwnerApprovalRecord | null {\n if (!value || typeof value !== 'object') return null\n const record = value as Partial<OwnerApprovalRecord>\n const hours = Number(record.hours)\n if (!Number.isInteger(hours) || hours < 1) return null\n if (!OWNER_APPROVAL_CHANNELS.includes(record.channel as OwnerApprovalChannel)) return null\n return {\n hours,\n rateUsd: Number(record.rateUsd) || 0,\n channel: record.channel as OwnerApprovalChannel,\n approvedAtMs: Number(record.approvedAtMs) || 0,\n recordedAtMs: Number(record.recordedAtMs) || 0,\n recordedByUid: String(record.recordedByUid ?? ''),\n recordedByEmail: record.recordedByEmail ?? null,\n note: record.note ?? null,\n version: Number(record.version) || 1,\n corrected: record.corrected === true,\n correctionReason: record.correctionReason ?? null,\n }\n}\n\n/** \"2 hours\", \"1 hour\". */\nexport function describeHours(hours: number): string {\n return `${hours} ${hours === 1 ? 'hour' : 'hours'}`\n}\n\n/**\n * The confirmation's one sentence, in the owner's words: what they agreed\n * to, how and when.\n */\nexport function ownerApprovalSentence(\n record: Pick<OwnerApprovalRecord, 'hours' | 'rateUsd' | 'channel' | 'approvedAtMs'>,\n what: string,\n formatDate: (ms: number) => string,\n): string {\n return (\n `You approved up to ${describeHours(record.hours)} of extra time at ` +\n `$${record.rateUsd} an hour for “${what}”, ` +\n `${OWNER_APPROVAL_CHANNEL_WORDS[record.channel]} on ${formatDate(record.approvedAtMs)}.`\n )\n}\n"],"names":["OWNER_APPROVAL_CHANNELS","OWNER_APPROVAL_CHANNEL_WORDS","phone","text","email","OWNER_APPROVAL_CHANNEL_LABELS","OWNER_APPROVAL_MAX_HOURS","OWNER_APPROVAL_NOTE_MAX","cleanText","value","max","trim","slice","readOwnerApprovalInput","raw","context","hours","Number","maxHours","isInteger","ok","error","channel","String","includes","approvedAtMs","isFinite","nowMs","previous","correctionReason","record","rateUsd","Math","floor","recordedAtMs","recordedByUid","by","uid","recordedByEmail","note","version","corrected","Boolean","readOwnerApprovalRecord","describeHours","ownerApprovalSentence","what","formatDate"],"mappings":"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"}