@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.
- package/dist/engine/extension-names.d.ts +12 -1
- package/dist/engine/extension-names.js +7 -0
- package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
- package/dist/engine/feature-ast/extractors/index.js +1 -1
- package/dist/engine/feature-ast/extractors/round5.d.ts +1 -0
- package/dist/engine/feature-ast/extractors/round5.js +5 -0
- package/dist/engine/feature-ast/parse.js +3 -1
- package/dist/engine/index.d.ts +3 -2
- package/dist/engine/index.js +2 -2
- package/dist/engine/role-assignment.d.ts +11 -1
- package/dist/engine/role-assignment.js +64 -3
- package/dist/i18n/required-surface-keys.js +19 -3
- package/dist/ui-types/index.d.ts +1 -1
- package/package.json +4 -4
|
@@ -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);
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -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 {
|
|
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";
|
package/dist/engine/index.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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 (
|
|
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
|
|
107
|
-
// toolbar button
|
|
108
|
-
|
|
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)) {
|
package/dist/ui-types/index.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
210
|
-
"@cosmicdrift/kumiko-types": "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.
|
|
226
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.333.0",
|
|
227
227
|
"bun-types": "^1.3.13",
|
|
228
228
|
"pino-pretty": "^13.1.3"
|
|
229
229
|
},
|