@cosmicdrift/kumiko-framework 0.332.0 → 0.333.0

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.
@@ -5,6 +5,7 @@ import type { PrincipalStatusPlugin, TenantLifecycleStatusPlugin } from "./activ
5
5
  import type { TenantDataExtensionHooks } from "./extensions/tenant-data.js";
6
6
  import type { TenantResourceExtensionHooks } from "./extensions/tenant-resource.js";
7
7
  import type { UserDataExtensionOptions } from "./extensions/user-data.js";
8
+ import type { AssignableFromRole } from "./role-assignment.js";
8
9
  import { TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin } from "./tier-resolver-extension.js";
9
10
  /**
10
11
  * `userData` — User-Daten-Rights-Hooks (DSGVO Art. 15 + 17 + 20).
@@ -137,13 +138,20 @@ export declare const EXT_PRINCIPAL_STATUS: "principalStatus";
137
138
  * the bundled `tenant-lifecycle` feature; absent means no tenant is ever "in teardown".
138
139
  */
139
140
  export declare const EXT_TENANT_LIFECYCLE_STATUS: "tenantLifecycleStatus";
141
+ /**
142
+ * `assignableRole` — an app declares a membership role as assignable:
143
+ * `r.useExtension(EXT_ASSIGNABLE_ROLE, "<RoleName>", { assignableFrom? })`
144
+ * (`assignableFrom` defaults to "Admin").
145
+ * Provider is the bundled `tenant` feature; consumers are the membership-role guards.
146
+ */
147
+ export declare const EXT_ASSIGNABLE_ROLE: "assignableRole";
140
148
  export declare const TENANT_MEMBERSHIPS_QUERY: "tenant:query:memberships";
141
149
  /**
142
150
  * Union aller standardisierten Extension-Namen der Datenschutz-Surface.
143
151
  * Nicht alle Extensions im System sind in dieser Liste — andere
144
152
  * Features koennen weiterhin eigene Extension-Namen registrieren.
145
153
  */
146
- export type KumikoExtensionName = typeof EXT_USER_DATA | typeof EXT_TENANT_DATA | typeof EXT_STORAGE_PROVIDER | typeof EXT_SEARCH_ADAPTER | typeof EXT_EXTERNAL_RESOURCE | typeof EXT_INFRA_RESOURCE | typeof EXT_FILE_PROVIDER | typeof EXT_DERIVATIVE_RENDERER | typeof EXT_DERIVATIVE_PUBLIC_PREDICATE | typeof EXT_DERIVATIVE_OVERLAY_RESOLVER | typeof EXT_PRINCIPAL_STATUS | typeof EXT_TENANT_LIFECYCLE_STATUS;
154
+ export type KumikoExtensionName = typeof EXT_USER_DATA | typeof EXT_TENANT_DATA | typeof EXT_STORAGE_PROVIDER | typeof EXT_SEARCH_ADAPTER | typeof EXT_EXTERNAL_RESOURCE | typeof EXT_INFRA_RESOURCE | typeof EXT_FILE_PROVIDER | typeof EXT_DERIVATIVE_RENDERER | typeof EXT_DERIVATIVE_PUBLIC_PREDICATE | typeof EXT_DERIVATIVE_OVERLAY_RESOLVER | typeof EXT_PRINCIPAL_STATUS | typeof EXT_TENANT_LIFECYCLE_STATUS | typeof EXT_ASSIGNABLE_ROLE;
147
155
  /** The four `destroyTenant(tenantId, ctx)`-only resource-cleanup extension points. */
148
156
  export type TenantResourceExtensionName = typeof EXT_STORAGE_PROVIDER | typeof EXT_SEARCH_ADAPTER | typeof EXT_EXTERNAL_RESOURCE | typeof EXT_INFRA_RESOURCE;
149
157
  declare module "@cosmicdrift/kumiko-framework/engine" {
@@ -155,6 +163,9 @@ declare module "@cosmicdrift/kumiko-framework/engine" {
155
163
  [EXT_DERIVATIVE_OVERLAY_RESOLVER]: OverlayResolverPlugin;
156
164
  [EXT_PRINCIPAL_STATUS]: PrincipalStatusPlugin;
157
165
  [EXT_TENANT_LIFECYCLE_STATUS]: TenantLifecycleStatusPlugin;
166
+ [EXT_ASSIGNABLE_ROLE]: {
167
+ readonly assignableFrom?: AssignableFromRole;
168
+ };
158
169
  [EXT_STORAGE_PROVIDER]: TenantResourceExtensionHooks;
159
170
  [EXT_SEARCH_ADAPTER]: TenantResourceExtensionHooks;
160
171
  [EXT_EXTERNAL_RESOURCE]: TenantResourceExtensionHooks;
@@ -167,6 +167,13 @@ export const EXT_PRINCIPAL_STATUS = "principalStatus";
167
167
  * the bundled `tenant-lifecycle` feature; absent means no tenant is ever "in teardown".
168
168
  */
169
169
  export const EXT_TENANT_LIFECYCLE_STATUS = "tenantLifecycleStatus";
170
+ /**
171
+ * `assignableRole` — an app declares a membership role as assignable:
172
+ * `r.useExtension(EXT_ASSIGNABLE_ROLE, "<RoleName>", { assignableFrom? })`
173
+ * (`assignableFrom` defaults to "Admin").
174
+ * Provider is the bundled `tenant` feature; consumers are the membership-role guards.
175
+ */
176
+ export const EXT_ASSIGNABLE_ROLE = "assignableRole";
170
177
  // Default membership-list query handler name. Accepted coupling, same
171
178
  // pattern as FILE_PROVIDER_CONFIG_KEY — the `tenant` feature registers a handler under this literal.
172
179
  export const TENANT_MEMBERSHIPS_QUERY = "tenant:query:memberships";
@@ -8,7 +8,7 @@ export { findImportBindingForLocalName, resolveModuleFile, } from "./resolve-imp
8
8
  export { extractDescribe, extractOptionalRequires, extractReadsConfig, extractRequires, extractSystemScope, extractToggleable, extractUiHints, } from "./round1.js";
9
9
  export { extractEntity, extractNav, extractRelation, extractWorkspace, } from "./round2.js";
10
10
  export { extractClaimKey, extractConfig, extractMetric, extractReferenceData, extractSecret, extractTranslations, extractUseExtension, isClaimKeyType, type NamedOptionsResult, readNamedOptions, } from "./round3.js";
11
- export { extractEnvSchema, extractExposesApi, extractExtendsRegistrar, extractStoreTable, extractUsesApi, } from "./round5.js";
11
+ export { extractBootCheck, extractEnvSchema, extractExposesApi, extractExtendsRegistrar, extractStoreTable, extractUsesApi, } from "./round5.js";
12
12
  export { extractTreeActions } from "./round6.js";
13
13
  export type { ExtractOutput } from "./shared.js";
14
14
  export { fail, findFunctionLiteral, isPlainObject, ok, readBooleanProperty, readDataLiteralNode, readNameOrRef, readNameOrRefOrList, readObjectPropertyInitializer, readPropertyKey, readStringLiteralArgs, readVarargsOrArrayProp, resolveSameFileObjectLiteral, } from "./shared.js";
@@ -8,6 +8,6 @@ export { findImportBindingForLocalName, resolveModuleFile, } from "./resolve-imp
8
8
  export { extractDescribe, extractOptionalRequires, extractReadsConfig, extractRequires, extractSystemScope, extractToggleable, extractUiHints, } from "./round1.js";
9
9
  export { extractEntity, extractNav, extractRelation, extractWorkspace, } from "./round2.js";
10
10
  export { extractClaimKey, extractConfig, extractMetric, extractReferenceData, extractSecret, extractTranslations, extractUseExtension, isClaimKeyType, readNamedOptions, } from "./round3.js";
11
- export { extractEnvSchema, extractExposesApi, extractExtendsRegistrar, extractStoreTable, extractUsesApi, } from "./round5.js";
11
+ export { extractBootCheck, extractEnvSchema, extractExposesApi, extractExtendsRegistrar, extractStoreTable, extractUsesApi, } from "./round5.js";
12
12
  export { extractTreeActions } from "./round6.js";
13
13
  export { fail, findFunctionLiteral, isPlainObject, ok, readBooleanProperty, readDataLiteralNode, readNameOrRef, readNameOrRefOrList, readObjectPropertyInitializer, readPropertyKey, readStringLiteralArgs, readVarargsOrArrayProp, resolveSameFileObjectLiteral, } from "./shared.js";
@@ -6,3 +6,4 @@ export declare function extractExtendsRegistrar(call: CallExpression, sourceFile
6
6
  export declare function extractUsesApi(call: CallExpression, sourceFile: SourceFile): ExtractOutput<UsesApiPattern>;
7
7
  export declare function extractExposesApi(call: CallExpression, sourceFile: SourceFile): ExtractOutput<ExposesApiPattern>;
8
8
  export declare function extractStoreTable(call: CallExpression, sourceFile: SourceFile): ExtractOutput<never>;
9
+ export declare function extractBootCheck(call: CallExpression, sourceFile: SourceFile): ExtractOutput<never>;
@@ -61,3 +61,8 @@ export function extractStoreTable(call, sourceFile) {
61
61
  // UnknownPattern) marks it design-time-unreadable, like entity-by-identifier.
62
62
  return fail("storeTable", sourceLocationFromNode(call, sourceFile), "storeTable meta is a factory/identifier argument, not a static literal");
63
63
  }
64
+ export function extractBootCheck(call, sourceFile) {
65
+ // The argument is a runtime closure over the composed feature list —
66
+ // nothing to extract statically, so a clean ParseError like storeTable.
67
+ return fail("bootCheck", sourceLocationFromNode(call, sourceFile), "bootCheck takes a runtime closure, not a static literal");
68
+ }
@@ -21,7 +21,7 @@
21
21
  // Per-pattern extractors fill in iteratively (C1.5) — each round adds
22
22
  // one extractor + a focused test.
23
23
  import { Project, SyntaxKind } from "ts-morph";
24
- import { extractAiClassify, extractAiExtract, extractAiGenerate, extractAuthClaims, extractClaimKey, extractConfig, extractDefineEvent, extractDescribe, extractEntity, extractEnvSchema, extractExposesApi, extractExtendsRegistrar, extractHook, extractHttpRoute, extractJob, extractMetric, extractMultiStreamProjection, extractNav, extractNotification, extractOptionalRequires, extractProjection, extractQueryHandler, extractReadsConfig, extractReferenceData, extractRelation, extractRequires, extractScreen, extractSecret, extractStoreTable, extractStreamHandler, extractSystemScope, extractToggleable, extractTranslations, extractTreeActions, extractUiHints, extractUseExtension, extractUsesApi, extractWorkspace, extractWriteHandler, findFunctionLiteral, findImportBindingForLocalName, readObjectPropertyInitializer, resolveModuleFile, resolveSameFileObjectLiteral, } from "./extractors/index.js";
24
+ import { extractAiClassify, extractAiExtract, extractAiGenerate, extractAuthClaims, extractBootCheck, extractClaimKey, extractConfig, extractDefineEvent, extractDescribe, extractEntity, extractEnvSchema, extractExposesApi, extractExtendsRegistrar, extractHook, extractHttpRoute, extractJob, extractMetric, extractMultiStreamProjection, extractNav, extractNotification, extractOptionalRequires, extractProjection, extractQueryHandler, extractReadsConfig, extractReferenceData, extractRelation, extractRequires, extractScreen, extractSecret, extractStoreTable, extractStreamHandler, extractSystemScope, extractToggleable, extractTranslations, extractTreeActions, extractUiHints, extractUseExtension, extractUsesApi, extractWorkspace, extractWriteHandler, findFunctionLiteral, findImportBindingForLocalName, readObjectPropertyInitializer, resolveModuleFile, resolveSameFileObjectLiteral, } from "./extractors/index.js";
25
25
  import { sourceLocationFromNode } from "./source-location.js";
26
26
  /**
27
27
  * Parse the given feature file and return all recognised r.* calls
@@ -440,6 +440,8 @@ function dispatchExtractor(methodName, call, sourceFile) {
440
440
  return extractExposesApi(call, sourceFile);
441
441
  case "storeTable":
442
442
  return extractStoreTable(call, sourceFile);
443
+ case "bootCheck":
444
+ return extractBootCheck(call, sourceFile);
443
445
  // Round 6 — Tree-Actions pattern
444
446
  case "treeActions":
445
447
  return extractTreeActions(call, sourceFile);
@@ -28,7 +28,7 @@ export { declareEscapeHatch } from "./escape-hatch-declaration.js";
28
28
  export type { EmitCtx } from "./event-helpers.js";
29
29
  export { emitEvent, typedPayload } from "./event-helpers.js";
30
30
  export type { KumikoExtensionName, TenantResourceExtensionName } from "./extension-names.js";
31
- export { EXT_DERIVATIVE_OVERLAY_RESOLVER, EXT_DERIVATIVE_PUBLIC_PREDICATE, EXT_DERIVATIVE_RENDERER, EXT_EXTERNAL_RESOURCE, EXT_FILE_PROVIDER, EXT_INFRA_RESOURCE, EXT_PRINCIPAL_STATUS, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, EXT_TENANT_LIFECYCLE_STATUS, EXT_USER_DATA, EXT_USER_DATA_ORDER, FILE_PROVIDER_CONFIG_KEY, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, TENANT_MEMBERSHIPS_QUERY, } from "./extension-names.js";
31
+ export { EXT_ASSIGNABLE_ROLE, EXT_DERIVATIVE_OVERLAY_RESOLVER, EXT_DERIVATIVE_PUBLIC_PREDICATE, EXT_DERIVATIVE_RENDERER, EXT_EXTERNAL_RESOURCE, EXT_FILE_PROVIDER, EXT_INFRA_RESOURCE, EXT_PRINCIPAL_STATUS, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, EXT_TENANT_LIFECYCLE_STATUS, EXT_USER_DATA, EXT_USER_DATA_ORDER, FILE_PROVIDER_CONFIG_KEY, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, TENANT_MEMBERSHIPS_QUERY, } from "./extension-names.js";
32
32
  export { EXTENSION_SELECTOR_HINT_KEY, extensionSelectorTargets, SELECTED_EXTENSIONS_QUERY, selectablePluginIds, } from "./extension-selector-plugins.js";
33
33
  export { extensionUsageEscapeHatchReason } from "./extensions/escape-hatch-usage.js";
34
34
  export type { StorageProviderDestroyTenantHook, StorageProviderExtensionHooks, StorageProviderHookCtx, } from "./extensions/storage-provider.js";
@@ -57,7 +57,8 @@ export { readClaim } from "./read-claim.js";
57
57
  export { createRegistry } from "./registry.js";
58
58
  export type { ClampInfo, ResolveOptions } from "./resolve-config-or-param.js";
59
59
  export { resolveConfigOrParam } from "./resolve-config-or-param.js";
60
- export { findForbiddenRoleAssignment } from "./role-assignment.js";
60
+ export type { AssignableAppRoles, AssignableFromRole } from "./role-assignment.js";
61
+ export { assignableAppRolesFromUsages, findForbiddenRoleAssignment, isAssignableByRole, } from "./role-assignment.js";
61
62
  export { runsInLane } from "./run-in.js";
62
63
  export type { StepListOutcome } from "./run-pipeline.js";
63
64
  export { runPipeline, runStepList } from "./run-pipeline.js";
@@ -26,7 +26,7 @@ export { createEntityExecutor, defineEntityCreateHandler, defineEntityDeleteHand
26
26
  defineEntityQueryHandler, defineEntityRestoreHandler, defineEntityUpdateHandler, defineEntityWriteHandler, defineProjectionQueryHandler, entityListSchema, MAX_LIST_LIMIT, registerEntityCrud, } from "./entity-handlers.js";
27
27
  export { declareEscapeHatch } from "./escape-hatch-declaration.js";
28
28
  export { emitEvent, typedPayload } from "./event-helpers.js";
29
- export { EXT_DERIVATIVE_OVERLAY_RESOLVER, EXT_DERIVATIVE_PUBLIC_PREDICATE, EXT_DERIVATIVE_RENDERER, EXT_EXTERNAL_RESOURCE, EXT_FILE_PROVIDER, EXT_INFRA_RESOURCE, EXT_PRINCIPAL_STATUS, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, EXT_TENANT_LIFECYCLE_STATUS, EXT_USER_DATA, EXT_USER_DATA_ORDER, FILE_PROVIDER_CONFIG_KEY, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, TENANT_MEMBERSHIPS_QUERY, } from "./extension-names.js";
29
+ export { EXT_ASSIGNABLE_ROLE, EXT_DERIVATIVE_OVERLAY_RESOLVER, EXT_DERIVATIVE_PUBLIC_PREDICATE, EXT_DERIVATIVE_RENDERER, EXT_EXTERNAL_RESOURCE, EXT_FILE_PROVIDER, EXT_INFRA_RESOURCE, EXT_PRINCIPAL_STATUS, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, EXT_TENANT_LIFECYCLE_STATUS, EXT_USER_DATA, EXT_USER_DATA_ORDER, FILE_PROVIDER_CONFIG_KEY, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, TENANT_MEMBERSHIPS_QUERY, } from "./extension-names.js";
30
30
  export { EXTENSION_SELECTOR_HINT_KEY, extensionSelectorTargets, SELECTED_EXTENSIONS_QUERY, selectablePluginIds, } from "./extension-selector-plugins.js";
31
31
  export { extensionUsageEscapeHatchReason } from "./extensions/escape-hatch-usage.js";
32
32
  export { isTenantDataExtensionHooks, } from "./extensions/tenant-data.js";
@@ -54,7 +54,7 @@ export { isValidQn, parseQn, QnTypes, qn, toKebab } from "./qualified-name.js";
54
54
  export { readClaim } from "./read-claim.js";
55
55
  export { createRegistry } from "./registry.js";
56
56
  export { resolveConfigOrParam } from "./resolve-config-or-param.js";
57
- export { findForbiddenRoleAssignment } from "./role-assignment.js";
57
+ export { assignableAppRolesFromUsages, findForbiddenRoleAssignment, isAssignableByRole, } from "./role-assignment.js";
58
58
  export { runsInLane } from "./run-in.js";
59
59
  export { runPipeline, runStepList } from "./run-pipeline.js";
60
60
  export { buildInsertSchema, buildUpdateSchema, fieldToZod } from "./schema-builder.js";
@@ -1 +1,11 @@
1
- export declare function findForbiddenRoleAssignment(actorRoles: readonly string[], assignedRoles: readonly string[], targetCurrentRoles: readonly string[]): string | undefined;
1
+ export type AssignableFromRole = "User" | "Member" | "Editor" | "Admin" | "TenantAdmin" | "SystemAdmin";
2
+ /** App role → lowest built-in role allowed to assign it. */
3
+ export type AssignableAppRoles = ReadonlyMap<string, AssignableFromRole>;
4
+ export declare const DEFAULT_APP_ROLE_ASSIGNABLE_FROM: AssignableFromRole;
5
+ export declare function assignableAppRolesFromUsages(usages: readonly {
6
+ entityName: string;
7
+ options?: Record<string, unknown> | undefined;
8
+ }[]): AssignableAppRoles;
9
+ /** True when `actorRole` ranks at least as high as the role an app role is assignable from. */
10
+ export declare function isAssignableByRole(assignableFrom: AssignableFromRole, actorRole: AssignableFromRole): boolean;
11
+ export declare function findForbiddenRoleAssignment(actorRoles: readonly string[], assignedRoles: readonly string[], targetCurrentRoles: readonly string[], assignableAppRoles?: AssignableAppRoles): string | undefined;
@@ -1,8 +1,13 @@
1
+ import { ROLES } from "../auth/roles.js";
2
+ import { isForbiddenMembershipRole } from "./membership-roles.js";
3
+ const PLATFORM_ROLE_NAMES = new Set(Object.values(ROLES));
1
4
  // Prototype-free rank table — Object literals leak Object.prototype
2
5
  // (`constructor`, `toString`, …) into `ROLE_RANKS[role]` lookups and turn
3
6
  // Math.max into NaN, which fails every `rank > actorRank` check open.
4
7
  const ROLE_RANKS = new Map([
5
8
  ["User", 0],
9
+ // Default seed role — unranked it would fail closed and never be assignable.
10
+ ["Member", 0],
6
11
  // Matches DEFAULT_INVITE_ROLE_OPTIONS — must stay ranked or invite UI fails closed.
7
12
  ["Editor", 1],
8
13
  ["Admin", 2],
@@ -10,7 +15,55 @@ const ROLE_RANKS = new Map([
10
15
  ["SystemAdmin", 4],
11
16
  ["system", 5],
12
17
  ]);
13
- // Unknown roles: +∞ on the assigned path (cannot grant what we don't know),
18
+ // App roles are ordinary, non-privileged tiers: whoever may manage members
19
+ // (access.admin) may grant them; an app raises the bar via assignableFrom.
20
+ export const DEFAULT_APP_ROLE_ASSIGNABLE_FROM = "Admin";
21
+ const NO_ASSIGNABLE_APP_ROLES = new Map();
22
+ const ASSIGNABLE_FROM_ROLES = new Set([
23
+ "User",
24
+ "Member",
25
+ "Editor",
26
+ "Admin",
27
+ "TenantAdmin",
28
+ "SystemAdmin",
29
+ ]);
30
+ function isAssignableFromRole(value) {
31
+ return typeof value === "string" && ASSIGNABLE_FROM_ROLES.has(value);
32
+ }
33
+ export function assignableAppRolesFromUsages(usages) {
34
+ const result = new Map();
35
+ for (const usage of usages) {
36
+ const role = usage.entityName;
37
+ if (role === "") {
38
+ throw new Error("assignableRole: role name must not be empty");
39
+ }
40
+ // Platform roles (TenantOwner, DataProtectionOfficer) gate handlers that
41
+ // TenantAdmin/Admin cannot reach — declaring one would let an Admin
42
+ // self-grant it, so they stay out of reach of app declarations.
43
+ if (ROLE_RANKS.has(role) || PLATFORM_ROLE_NAMES.has(role)) {
44
+ throw new Error(`assignableRole: "${role}" is a built-in role and cannot be redeclared`);
45
+ }
46
+ if (isForbiddenMembershipRole(role)) {
47
+ throw new Error(`assignableRole: "${role}" is a reserved role and cannot be a membership role`);
48
+ }
49
+ const declaredAssignableFrom = usage.options?.["assignableFrom"];
50
+ if (declaredAssignableFrom !== undefined && !isAssignableFromRole(declaredAssignableFrom)) {
51
+ throw new Error(`assignableRole: "${role}" has invalid assignableFrom, expected one of ${[...ASSIGNABLE_FROM_ROLES].join(", ")}`);
52
+ }
53
+ const assignableFrom = declaredAssignableFrom ?? DEFAULT_APP_ROLE_ASSIGNABLE_FROM;
54
+ const existing = result.get(role);
55
+ if (existing !== undefined && existing !== assignableFrom) {
56
+ throw new Error(`assignableRole: "${role}" is declared with conflicting assignableFrom (${existing} vs ${assignableFrom})`);
57
+ }
58
+ result.set(role, assignableFrom);
59
+ }
60
+ return result;
61
+ }
62
+ /** True when `actorRole` ranks at least as high as the role an app role is assignable from. */
63
+ export function isAssignableByRole(assignableFrom, actorRole) {
64
+ return getRoleRank(assignableFrom) <= getRoleRank(actorRole);
65
+ }
66
+ // Unknown roles: +∞ on the assigned path (cannot grant what we don't know, unless declared),
14
67
  // -1 on the actor path (cannot elevate via an unrecognized self-role).
15
68
  function roleRankOr(role, unknownRank) {
16
69
  return ROLE_RANKS.get(role) ?? unknownRank;
@@ -27,15 +80,23 @@ function maxRoleRank(roles) {
27
80
  function knownRoleRank(role) {
28
81
  return ROLE_RANKS.get(role);
29
82
  }
83
+ // Built-ins win: a declaration can never re-tier a built-in role.
84
+ function requiredAssignRank(role, assignableAppRoles) {
85
+ const builtIn = ROLE_RANKS.get(role);
86
+ if (builtIn !== undefined)
87
+ return builtIn;
88
+ const assignableFrom = assignableAppRoles.get(role);
89
+ return assignableFrom === undefined ? Number.POSITIVE_INFINITY : getRoleRank(assignableFrom);
90
+ }
30
91
  export function findForbiddenRoleAssignment(actorRoles, assignedRoles,
31
92
  // Required so callers cannot accidentally disable the downgrade/
32
93
  // takeover guard by omitting the third argument (new users: pass []).
33
- targetCurrentRoles) {
94
+ targetCurrentRoles, assignableAppRoles = NO_ASSIGNABLE_APP_ROLES) {
34
95
  const actorRank = maxRoleRank(actorRoles);
35
96
  // Assign path: fail-closed on unknown / above-actor roles — except unranked
36
97
  // app roles the target already holds (round-trip restore after strip).
37
98
  const forbiddenAssigned = assignedRoles.find((role) => {
38
- if (getRoleRank(role) <= actorRank)
99
+ if (requiredAssignRank(role, assignableAppRoles) <= actorRank)
39
100
  return false;
40
101
  if (knownRoleRank(role) === undefined && targetCurrentRoles.includes(role))
41
102
  return false;
@@ -42,6 +42,7 @@ function editFieldName(f) {
42
42
  // Group titles are translated exactly like the section title (computeEditViewModel).
43
43
  function pushSectionTitles(out, section, treatAsKey = false) {
44
44
  pushKey(out, section.title, treatAsKey);
45
+ pushKey(out, section.subtitle, treatAsKey);
45
46
  for (const group of section.groups ?? [])
46
47
  pushKey(out, group.title, treatAsKey);
47
48
  }
@@ -51,6 +52,10 @@ function pushRowActionKeys(out, action) {
51
52
  pushKey(out, action.confirm);
52
53
  pushKey(out, action.confirmLabel);
53
54
  }
55
+ if (action.kind === "drawer") {
56
+ pushKey(out, action.title);
57
+ pushKey(out, action.subtitle);
58
+ }
54
59
  }
55
60
  function pushDashboardScreenKeys(out, dashboard) {
56
61
  for (const panel of dashboard.panels)
@@ -103,9 +108,12 @@ function pushToolbarActionKeys(out, action) {
103
108
  pushKey(out, action.confirm);
104
109
  pushKey(out, action.confirmLabel);
105
110
  }
106
- // drawer-kind carries no confirm/confirmLabel — action.label (the
107
- // toolbar button AND the Drawer title, see ToolbarDrawerHost) is
108
- // already pushed above.
111
+ // drawer-kind carries no confirm/confirmLabel — action.label is the
112
+ // toolbar button and the Drawer title unless `title` overrides it.
113
+ if (action.kind === "drawer") {
114
+ pushKey(out, action.title);
115
+ pushKey(out, action.subtitle);
116
+ }
109
117
  }
110
118
  function pushRelatedListSectionKeys(out, section) {
111
119
  pushKey(out, section.title);
@@ -180,6 +188,7 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
180
188
  case "entityEdit": {
181
189
  const edit = screen;
182
190
  pushKey(out, edit.submitLabel);
191
+ pushKey(out, edit.titleTemplate);
183
192
  for (const section of edit.layout.sections) {
184
193
  if (isExtensionEditSection(section)) {
185
194
  pushKey(out, section.title);
@@ -204,6 +213,8 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
204
213
  pushKey(out, form.submitLabel);
205
214
  pushKey(out, form.summary?.title);
206
215
  pushKey(out, form.summary?.subtitle);
216
+ for (const action of form.footerActions ?? [])
217
+ pushKey(out, action.label);
207
218
  for (const fieldName of Object.keys(form.fields)) {
208
219
  const override = form.fieldLabels?.[fieldName];
209
220
  if (override !== undefined)
@@ -328,6 +339,11 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
328
339
  }
329
340
  if (section.kind === "relatedList") {
330
341
  pushRelatedListSectionKeys(out, section);
342
+ if (section.groupBy !== undefined) {
343
+ pushKey(out, section.groupBy.label);
344
+ for (const label of Object.values(section.groupBy.labels ?? {}))
345
+ pushKey(out, label);
346
+ }
331
347
  continue;
332
348
  }
333
349
  if (isWriteFormEditSection(section)) {
@@ -9,7 +9,7 @@ export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefin
9
9
  export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, RoleAccessPersonalData, RoleAccessRule, } from "../engine/types/handlers.js";
10
10
  export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
11
11
  export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav.js";
12
- export type { ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartPanel, DashboardChartTone, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardTimeRangeDefinition, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSubtitlePart, RelatedListToolbarAction, RowAction, RowActionDrawer, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.js";
12
+ export type { ActionFormFooterAction, ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartPanel, DashboardChartTone, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardTimeRangeDefinition, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListFacetConfig, EntityListFacetExtraOption, EntityListScreenDefinition, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSubtitlePart, RelatedListGroupBy, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionDisplay, RowActionDrawer, RowActionMode, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.js";
13
13
  export { metricField } from "../engine/types/screen.js";
14
14
  export type { TargetRef } from "../engine/types/target-ref.js";
15
15
  export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.332.0",
3
+ "version": "0.333.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -206,8 +206,8 @@
206
206
  "./package.json": "./package.json"
207
207
  },
208
208
  "dependencies": {
209
- "@cosmicdrift/kumiko-http": "0.332.0",
210
- "@cosmicdrift/kumiko-types": "0.332.0",
209
+ "@cosmicdrift/kumiko-http": "0.333.0",
210
+ "@cosmicdrift/kumiko-types": "0.333.0",
211
211
  "bullmq": "^5.76.7",
212
212
  "bun-types": "^1.3.13",
213
213
  "hono": "^4.13.1",
@@ -223,7 +223,7 @@
223
223
  "zod": "^4.4.3"
224
224
  },
225
225
  "devDependencies": {
226
- "@cosmicdrift/kumiko-dispatcher-live": "0.332.0",
226
+ "@cosmicdrift/kumiko-dispatcher-live": "0.333.0",
227
227
  "bun-types": "^1.3.13",
228
228
  "pino-pretty": "^13.1.3"
229
229
  },