@cosmicdrift/kumiko-framework 0.48.0 → 0.50.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.
Files changed (56) hide show
  1. package/package.json +1 -1
  2. package/src/api/__tests__/origin-middleware.integration.test.ts +132 -0
  3. package/src/api/__tests__/origin-middleware.test.ts +191 -0
  4. package/src/api/anonymous-cookie.ts +1 -0
  5. package/src/api/api-constants.ts +9 -0
  6. package/src/api/auth-middleware.ts +2 -0
  7. package/src/api/auth-routes.ts +29 -4
  8. package/src/api/csrf-middleware.ts +1 -4
  9. package/src/api/origin-middleware.ts +98 -0
  10. package/src/api/server.ts +20 -0
  11. package/src/bun-db/__tests__/_helpers.ts +1 -0
  12. package/src/bun-db/query.ts +1 -0
  13. package/src/db/__tests__/migrate-generator.test.ts +11 -0
  14. package/src/db/collect-table-metas.ts +2 -1
  15. package/src/db/dialect.ts +3 -1
  16. package/src/db/migrate-generator.ts +6 -2
  17. package/src/db/queries/event-store.ts +1 -0
  18. package/src/engine/__tests__/boot-validator.test.ts +23 -0
  19. package/src/engine/__tests__/build-app-schema-settings-hub.test.ts +118 -0
  20. package/src/engine/__tests__/build-config-feature-schema.test.ts +174 -0
  21. package/src/engine/__tests__/config-helpers.test.ts +57 -0
  22. package/src/engine/__tests__/feature-manifest.test.ts +45 -0
  23. package/src/engine/boot-validator/config-deps.ts +26 -0
  24. package/src/engine/boot-validator/index.ts +2 -0
  25. package/src/engine/build-app-schema.ts +45 -0
  26. package/src/engine/build-config-feature-schema.ts +228 -0
  27. package/src/engine/config-helpers.ts +25 -0
  28. package/src/engine/define-handler.ts +1 -0
  29. package/src/engine/entity-handlers.ts +7 -1
  30. package/src/engine/feature-manifest.ts +4 -3
  31. package/src/engine/index.ts +8 -0
  32. package/src/engine/pipeline.ts +1 -0
  33. package/src/engine/qualified-name.ts +1 -0
  34. package/src/engine/state-machine.ts +1 -0
  35. package/src/engine/steps/compute.ts +1 -1
  36. package/src/engine/steps/return.ts +1 -0
  37. package/src/engine/types/config.ts +35 -0
  38. package/src/engine/types/index.ts +2 -0
  39. package/src/engine/types/screen.ts +13 -0
  40. package/src/env/index.ts +1 -0
  41. package/src/errors/write-error-info.ts +2 -0
  42. package/src/es-ops/__tests__/context.integration.test.ts +33 -24
  43. package/src/es-ops/__tests__/runner.integration.test.ts +130 -86
  44. package/src/es-ops/context.ts +6 -4
  45. package/src/event-store/event-store.ts +3 -0
  46. package/src/files/file-handle.ts +1 -1
  47. package/src/migrations/__tests__/pending-rebuilds.integration.test.ts +62 -1
  48. package/src/migrations/index.ts +1 -0
  49. package/src/migrations/pending-rebuilds.ts +48 -9
  50. package/src/pipeline/dispatcher.ts +5 -5
  51. package/src/pipeline/lifecycle-pipeline.ts +4 -4
  52. package/src/stack/table-helpers.ts +1 -0
  53. package/src/time/tz-context.ts +3 -3
  54. package/src/utils/compare.ts +10 -0
  55. package/src/utils/ids.ts +1 -0
  56. package/src/utils/index.ts +1 -0
@@ -0,0 +1,228 @@
1
+ // Self-Populating Settings-Hub (config-provisioning Phase 2).
2
+ //
3
+ // Leitet aus den im Registry deklarierten Config-Keys automatisch die
4
+ // Settings-UI ab: pro Audience (scope) einen Parent-Nav, pro (Feature ×
5
+ // scope) einen configEdit-Screen + Child-Nav darunter. Kein manuelles
6
+ // r.screen/r.nav am App-Author.
7
+ //
8
+ // Sichtbar wird nur ein Key MIT `mask` (siehe ConfigKeyDefinition): mask ist
9
+ // die per-Key-Intent „user-facing Einstellung" und trägt zugleich das Label
10
+ // (mask.title, ein i18n-Key). Keys ohne mask sind internes Plumbing
11
+ // (ENV-provisioniert/computed) und erscheinen nicht.
12
+ //
13
+ // Die erzeugten Screens/Navs werden von buildAppSchema in die FeatureSchema
14
+ // des config-Features (featureName "config") gemerged — der Renderer
15
+ // qualifiziert die kurzen ids/refs mit "config". Daher hier durchweg KURZE
16
+ // ids/parent/screen-Refs (buildNavRegistrySliceForApp qualifiziert selbst).
17
+
18
+ import type { WorkspaceSchema } from "../ui-types";
19
+ import type { ConfigScope } from "./constants";
20
+ import {
21
+ createBooleanField,
22
+ createNumberField,
23
+ createSelectField,
24
+ createTextField,
25
+ } from "./factories";
26
+ import type { ConfigKeyDefinition } from "./types/config";
27
+ import type { Registry } from "./types/feature";
28
+ import type { FieldDefinition } from "./types/fields";
29
+ import type { AccessRule } from "./types/handlers";
30
+ import type { NavDefinition } from "./types/nav";
31
+ import type {
32
+ ConfigEditScreenDefinition,
33
+ EditFieldsSection,
34
+ ScreenDefinition,
35
+ } from "./types/screen";
36
+
37
+ // Namespace, unter dem buildAppSchema die generierten Screens/Navs einhängt
38
+ // (find-or-create FeatureSchema). MUSS gleich CONFIG_FEATURE aus dem config
39
+ // bundled-feature sein — framework kann das const nicht importieren (Richtung
40
+ // bundled-features → framework), darum hier gepinnt + Pin-Test bundled-seitig.
41
+ export const SETTINGS_HUB_FEATURE = "config";
42
+ // Eigene Workspace nur für workspace-mode-Apps (siehe buildAppSchema): Settings
43
+ // erscheinen als eigener Switcher-Eintrag, statt die kuratierten App-Workspaces
44
+ // zu verschmutzen. Apps ohne Workspaces zeigen die Navs über den no-filter-Pfad.
45
+ export const SETTINGS_HUB_WORKSPACE = "settings";
46
+
47
+ export type ConfigFeatureSchema = {
48
+ readonly screens: readonly ScreenDefinition[];
49
+ readonly navs: readonly NavDefinition[];
50
+ // Fertige Settings-Workspace mit qualifizierten navMembers. Nur present
51
+ // wenn mind. ein Key opt-in via mask hat; buildAppSchema hängt sie NUR an
52
+ // wenn die App bereits Workspaces nutzt (sonst kippt eine workspace-lose
53
+ // App in den Filter-Modus und verliert alle übrigen Navs).
54
+ readonly workspace?: WorkspaceSchema;
55
+ };
56
+
57
+ // Audience-Reihenfolge im Sidebar: Plattform vor Tenant vor Benutzer.
58
+ const SCOPE_ORDER: Record<ConfigScope, number> = { system: 10, tenant: 20, user: 30 };
59
+
60
+ type MaskedKey = {
61
+ readonly qn: string;
62
+ readonly feature: string;
63
+ readonly shortKey: string;
64
+ readonly def: ConfigKeyDefinition;
65
+ };
66
+
67
+ export function buildConfigFeatureSchema(registry: Registry): ConfigFeatureSchema {
68
+ const masked = collectMaskedKeys(registry);
69
+ if (masked.length === 0) return { screens: [], navs: [] };
70
+
71
+ const screens: ScreenDefinition[] = [];
72
+ const navs: NavDefinition[] = [];
73
+
74
+ for (const scope of scopesPresent(masked)) {
75
+ const scopeKeys = masked.filter((k) => k.def.scope === scope);
76
+
77
+ // Audience-Parent: Gruppierungs-Knoten ohne Screen.
78
+ navs.push({
79
+ id: `audience-${scope}`,
80
+ label: `config.settings.${scope}`,
81
+ order: SCOPE_ORDER[scope],
82
+ access: unionEditAccess(scopeKeys.map((k) => k.def)),
83
+ });
84
+
85
+ for (const feature of featuresPresent(scopeKeys)) {
86
+ const group = scopeKeys.filter((k) => k.feature === feature);
87
+ const ordered = sortByMaskOrder(group);
88
+ const shortId = `${feature}-${scope}`;
89
+
90
+ screens.push(buildScreen(shortId, scope, feature, ordered));
91
+ navs.push({
92
+ id: shortId,
93
+ label: `${feature}.settings`,
94
+ parent: `audience-${scope}`,
95
+ screen: shortId,
96
+ order: minMaskOrder(group),
97
+ access: unionEditAccess(group.map((k) => k.def)),
98
+ });
99
+ }
100
+ }
101
+
102
+ return { screens, navs, workspace: buildSettingsWorkspace(navs, masked) };
103
+ }
104
+
105
+ // navMembers tragen die QUALIFIZIERTEN Nav-QNs (siehe build-app-schema.test:
106
+ // admin.navMembers === ["orders:nav:list", ...]). Die generierten Navs leben
107
+ // unter SETTINGS_HUB_FEATURE, also `config:nav:<shortId>`. Sortiert = stabile
108
+ // Landing-Screen-Wahl (firstNavScreenId iteriert navMembers der Reihe nach).
109
+ function buildSettingsWorkspace(
110
+ navs: readonly NavDefinition[],
111
+ masked: readonly MaskedKey[],
112
+ ): WorkspaceSchema {
113
+ const navMembers = navs.map((n) => `${SETTINGS_HUB_FEATURE}:nav:${n.id}`).sort();
114
+ return {
115
+ definition: {
116
+ id: SETTINGS_HUB_WORKSPACE,
117
+ label: "config.settings.title",
118
+ icon: "settings",
119
+ order: 1000,
120
+ // Union der Schreib-Rollen aller Hub-Keys — sonst sieht ein
121
+ // unprivilegierter User einen leeren "Settings"-Switcher-Eintrag.
122
+ access: unionEditAccess(masked.map((k) => k.def)),
123
+ },
124
+ navMembers,
125
+ };
126
+ }
127
+
128
+ function buildScreen(
129
+ shortId: string,
130
+ scope: ConfigScope,
131
+ feature: string,
132
+ keys: readonly MaskedKey[],
133
+ ): ConfigEditScreenDefinition {
134
+ const configKeys: Record<string, string> = {};
135
+ const fields: Record<string, FieldDefinition> = {};
136
+ const fieldLabels: Record<string, string> = {};
137
+ for (const k of keys) {
138
+ configKeys[k.shortKey] = k.qn;
139
+ fields[k.shortKey] = deriveField(k.def);
140
+ // mask is the visibility gate, so collectMaskedKeys guarantees it here.
141
+ if (k.def.mask) fieldLabels[k.shortKey] = k.def.mask.title;
142
+ }
143
+ const section: EditFieldsSection = {
144
+ title: `${feature}.settings`,
145
+ fields: keys.map((k) => k.shortKey),
146
+ };
147
+ return {
148
+ id: shortId,
149
+ type: "configEdit",
150
+ scope,
151
+ configKeys,
152
+ fields,
153
+ fieldLabels,
154
+ layout: { sections: [section] },
155
+ access: unionEditAccess(keys.map((k) => k.def)),
156
+ };
157
+ }
158
+
159
+ function deriveField(def: ConfigKeyDefinition): FieldDefinition {
160
+ switch (def.type) {
161
+ case "number":
162
+ return createNumberField();
163
+ case "boolean":
164
+ return createBooleanField();
165
+ case "select":
166
+ return def.options !== undefined && def.options.length > 0
167
+ ? createSelectField({ options: def.options })
168
+ : createTextField();
169
+ default:
170
+ return createTextField();
171
+ }
172
+ }
173
+
174
+ function collectMaskedKeys(registry: Registry): MaskedKey[] {
175
+ const out: MaskedKey[] = [];
176
+ for (const [qn, def] of registry.getAllConfigKeys()) {
177
+ // computed keys derive their value — there is no row to set, so a
178
+ // configEdit screen could not write them. Skip even when masked.
179
+ if (def.mask === undefined || def.computed !== undefined) continue;
180
+ const sep = qn.indexOf(":config:");
181
+ if (sep === -1) continue;
182
+ out.push({
183
+ qn,
184
+ feature: qn.slice(0, sep),
185
+ shortKey: qn.slice(sep + ":config:".length),
186
+ def,
187
+ });
188
+ }
189
+ return out;
190
+ }
191
+
192
+ function scopesPresent(keys: readonly MaskedKey[]): ConfigScope[] {
193
+ const set = new Set<ConfigScope>(keys.map((k) => k.def.scope));
194
+ return [...set].sort((a, b) => (SCOPE_ORDER[a] ?? 0) - (SCOPE_ORDER[b] ?? 0));
195
+ }
196
+
197
+ function featuresPresent(keys: readonly MaskedKey[]): string[] {
198
+ return [...new Set(keys.map((k) => k.feature))].sort();
199
+ }
200
+
201
+ function sortByMaskOrder(keys: readonly MaskedKey[]): MaskedKey[] {
202
+ return [...keys].sort(
203
+ (a, b) => maskOrder(a) - maskOrder(b) || a.shortKey.localeCompare(b.shortKey),
204
+ );
205
+ }
206
+
207
+ function maskOrder(k: MaskedKey): number {
208
+ return k.def.mask?.order ?? 0;
209
+ }
210
+
211
+ function minMaskOrder(keys: readonly MaskedKey[]): number {
212
+ return Math.min(...keys.map(maskOrder));
213
+ }
214
+
215
+ // Der Hub ist zum Editieren — wer mindestens einen Key der Gruppe SCHREIBEN
216
+ // darf, sieht den Settings-Eintrag (write, nicht read). Das hält system-scope
217
+ // (write default `["system"]`) human-hidden bis der Autor write: SystemAdmin
218
+ // opt-int, und zeigt user-scope (write `all`) jedem. `all` lässt sich in
219
+ // AccessRule nur als openToAll ausdrücken; der Write bleibt server-seitig
220
+ // per Key gegated.
221
+ function unionEditAccess(defs: readonly ConfigKeyDefinition[]): AccessRule {
222
+ const roles = new Set<string>();
223
+ for (const def of defs) {
224
+ for (const role of def.access.write) roles.add(role);
225
+ }
226
+ if (roles.has("all")) return { openToAll: true };
227
+ return { roles: [...roles] };
228
+ }
@@ -1,9 +1,11 @@
1
1
  import type { ConfigScope } from "./constants";
2
2
  import type {
3
+ ConfigBacking,
3
4
  ConfigBounds,
4
5
  ConfigComputedFn,
5
6
  ConfigKeyDefinition,
6
7
  ConfigKeyType,
8
+ ConfigMask,
7
9
  ConfigSeedDef,
8
10
  ConfigValue,
9
11
  CreateSeedOptions,
@@ -43,6 +45,18 @@ export const access = {
43
45
  // `allowPerRequest` is conditional against `text`: text keys can never
44
46
  // opt in to per-request overrides (XSS/SQL/Shell risk). For other types
45
47
  // it's a plain boolean opt-in consumed by resolveConfigOrParam.
48
+ //
49
+ // Provisioning metadata (optional, available on every scope so a key never
50
+ // switches factory to gain it):
51
+ // `env` binds an ENV var as the boot-time default (the
52
+ // ENV→app-override bridge reads keyDef.env).
53
+ // `inheritedToTenant` default true; false redacts the inherited system
54
+ // value from tenant-side reads (e.g. SMTP creds).
55
+ // `backing` storage backing; "secrets" routes the key through the
56
+ // secrets store. backing×scope rules are enforced at
57
+ // boot, not by this type (secrets don't cascade).
58
+ // `mask` marks the key as a user-facing setting → the
59
+ // Settings-Hub derives its screen+nav. Absent = internal.
46
60
  type ConfigKeyOptions<T extends ConfigKeyType> = {
47
61
  write?: readonly string[];
48
62
  read?: readonly string[];
@@ -53,6 +67,10 @@ type ConfigKeyOptions<T extends ConfigKeyType> = {
53
67
  computed?: ConfigComputedFn<T>;
54
68
  allowPerRequest?: T extends "text" ? never : boolean;
55
69
  required?: boolean;
70
+ env?: string;
71
+ inheritedToTenant?: boolean;
72
+ backing?: ConfigBacking;
73
+ mask?: ConfigMask;
56
74
  };
57
75
 
58
76
  // --- Scope Defaults ---
@@ -85,6 +103,10 @@ function createConfigKey<T extends ConfigKeyType>(
85
103
  computed: opts.computed,
86
104
  ...(opts.allowPerRequest === true ? { allowPerRequest: true } : {}),
87
105
  ...(opts.required === true ? { required: true } : {}),
106
+ ...(opts.env ? { env: opts.env } : {}),
107
+ ...(opts.inheritedToTenant === false ? { inheritedToTenant: false } : {}),
108
+ ...(opts.backing === "secrets" ? { backing: "secrets" } : {}),
109
+ ...(opts.mask ? { mask: opts.mask } : {}),
88
110
  };
89
111
  }
90
112
 
@@ -92,6 +114,7 @@ function createConfigKey<T extends ConfigKeyType>(
92
114
  // Generic on the type-tag so `r.config({keys})` can propagate it into the
93
115
  // returned `ConfigKeyHandle<T>` — that's what narrows `ctx.config(handle)`.
94
116
 
117
+ // @wrapper-known semantic-alias
95
118
  export function createTenantConfig<T extends ConfigKeyType>(
96
119
  type: T,
97
120
  opts?: ConfigKeyOptions<T>,
@@ -99,6 +122,7 @@ export function createTenantConfig<T extends ConfigKeyType>(
99
122
  return createConfigKey("tenant", type, opts);
100
123
  }
101
124
 
125
+ // @wrapper-known semantic-alias
102
126
  export function createSystemConfig<T extends ConfigKeyType>(
103
127
  type: T,
104
128
  opts?: ConfigKeyOptions<T>,
@@ -106,6 +130,7 @@ export function createSystemConfig<T extends ConfigKeyType>(
106
130
  return createConfigKey("system", type, opts);
107
131
  }
108
132
 
133
+ // @wrapper-known semantic-alias
109
134
  export function createUserConfig<T extends ConfigKeyType>(
110
135
  type: T,
111
136
  opts?: ConfigKeyOptions<T>,
@@ -122,6 +122,7 @@ export function defineWriteHandler<
122
122
 
123
123
  if ("perform" in def && def.perform !== undefined) {
124
124
  const performDef = def.perform;
125
+ // @wrapper-known semantic-alias
125
126
  const compiledHandler = async (
126
127
  event: WriteEvent<z.infer<TSchema>>,
127
128
  ctx: HandlerContext<TMap>,
@@ -261,6 +261,7 @@ export function defineEntityQueryHandler(
261
261
 
262
262
  type EntityHandlerOptions = { readonly access?: AccessRule };
263
263
 
264
+ // @wrapper-known semantic-alias
264
265
  export function defineEntityCreateHandler(
265
266
  entityName: string,
266
267
  entity: EntityDefinition,
@@ -269,6 +270,7 @@ export function defineEntityCreateHandler(
269
270
  return defineEntityWriteHandler(`${entityName}:create`, entity, options);
270
271
  }
271
272
 
273
+ // @wrapper-known semantic-alias
272
274
  export function defineEntityUpdateHandler(
273
275
  entityName: string,
274
276
  entity: EntityDefinition,
@@ -277,6 +279,7 @@ export function defineEntityUpdateHandler(
277
279
  return defineEntityWriteHandler(`${entityName}:update`, entity, options);
278
280
  }
279
281
 
282
+ // @wrapper-known semantic-alias
280
283
  export function defineEntityDeleteHandler(
281
284
  entityName: string,
282
285
  entity: EntityDefinition,
@@ -285,6 +288,7 @@ export function defineEntityDeleteHandler(
285
288
  return defineEntityWriteHandler(`${entityName}:delete`, entity, options);
286
289
  }
287
290
 
291
+ // @wrapper-known semantic-alias
288
292
  export function defineEntityRestoreHandler(
289
293
  entityName: string,
290
294
  entity: EntityDefinition,
@@ -293,6 +297,7 @@ export function defineEntityRestoreHandler(
293
297
  return defineEntityWriteHandler(`${entityName}:restore`, entity, options);
294
298
  }
295
299
 
300
+ // @wrapper-known semantic-alias
296
301
  export function defineEntityListHandler(
297
302
  entityName: string,
298
303
  entity: EntityDefinition,
@@ -301,6 +306,7 @@ export function defineEntityListHandler(
301
306
  return defineEntityQueryHandler(`${entityName}:list`, entity, options);
302
307
  }
303
308
 
309
+ // @wrapper-known semantic-alias
304
310
  export function defineEntityDetailHandler(
305
311
  entityName: string,
306
312
  entity: EntityDefinition,
@@ -359,7 +365,7 @@ export function defineProjectionQueryHandler(
359
365
  ctx.queryProjection(
360
366
  projectionQualifiedName,
361
367
  options?.unsafeAllTenants ? { unsafeAllTenants: true } : undefined,
362
- ),
368
+ ), // @wrapper-known semantic-alias
363
369
  ...(options?.access && { access: options.access }),
364
370
  };
365
371
  }
@@ -6,6 +6,7 @@
6
6
  // AST-Parser kann die imperativen Factory-Helper der bundled features nicht
7
7
  // lesen.
8
8
 
9
+ import { compareByCodepoint } from "../utils";
9
10
  import type { Registry } from "./types/feature";
10
11
 
11
12
  export type ManifestConfigKey = {
@@ -111,8 +112,8 @@ export function buildManifestFromRegistry(
111
112
  });
112
113
  }
113
114
 
114
- configKeys.sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName));
115
- secrets.sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName));
115
+ configKeys.sort((a, b) => compareByCodepoint(a.qualifiedName, b.qualifiedName));
116
+ secrets.sort((a, b) => compareByCodepoint(a.qualifiedName, b.qualifiedName));
116
117
 
117
118
  manifestFeatures.push({
118
119
  name: feature.name,
@@ -133,7 +134,7 @@ export function buildManifestFromRegistry(
133
134
  });
134
135
  }
135
136
 
136
- manifestFeatures.sort((a, b) => a.name.localeCompare(b.name));
137
+ manifestFeatures.sort((a, b) => compareByCodepoint(a.name, b.name));
137
138
 
138
139
  return {
139
140
  source: options.source,
@@ -3,6 +3,12 @@
3
3
  export { hasAccess } from "./access";
4
4
  export { validateBoot } from "./boot-validator";
5
5
  export { buildAppSchema } from "./build-app-schema";
6
+ export type { ConfigFeatureSchema } from "./build-config-feature-schema";
7
+ export {
8
+ buildConfigFeatureSchema,
9
+ SETTINGS_HUB_FEATURE,
10
+ SETTINGS_HUB_WORKSPACE,
11
+ } from "./build-config-feature-schema";
6
12
  export { buildTarget } from "./build-target";
7
13
  export {
8
14
  access,
@@ -215,6 +221,7 @@ export type {
215
221
  ConcurrencyMode,
216
222
  ConfigAccessor,
217
223
  ConfigAccessorFactory,
224
+ ConfigBacking,
218
225
  ConfigCascade,
219
226
  ConfigCascadeLevel,
220
227
  ConfigDefinition,
@@ -223,6 +230,7 @@ export type {
223
230
  ConfigKeyDefinition,
224
231
  ConfigKeyHandle,
225
232
  ConfigKeyType,
233
+ ConfigMask,
226
234
  ConfigResolver,
227
235
  ConfigScope,
228
236
  ConfigSeedDef,
@@ -80,6 +80,7 @@ export function pipeline<TPayload = unknown, TData = unknown>(
80
80
 
81
81
  // Internal: invoked by run-pipeline.ts to materialise the step list.
82
82
  // Not exported from the engine barrel — pipeline-internal plumbing.
83
+ // @wrapper-known semantic-alias
83
84
  export function buildPipelineSteps<TPayload>(
84
85
  pipelineDef: PipelineDef<TPayload>,
85
86
  event: WriteEvent<TPayload>,
@@ -92,6 +92,7 @@ export function isValidQn(value: string): boolean {
92
92
  // True if `name` is a valid QN segment (lowercase letters, digits, dashes;
93
93
  // starts with a letter). Same rule as `QN_SEGMENT` — kept public so feature
94
94
  // registration can reject bad names at the source instead of at registry-boot.
95
+ // @wrapper-known semantic-alias
95
96
  export function isKebabSegment(name: string): boolean {
96
97
  return QN_SEGMENT.test(name);
97
98
  }
@@ -61,6 +61,7 @@ export function defineTransitions<const TMap extends Record<string, readonly str
61
61
  * nutzen. Beides ist erlaubt; die Method-Form auf dem Graph ist die
62
62
  * idiomatische API für neuen Code.
63
63
  */
64
+ // @wrapper-known semantic-alias
64
65
  export function guardTransition<TStates extends string>(
65
66
  transitions: TransitionGraph<TStates>,
66
67
  from: TStates,
@@ -27,7 +27,7 @@ defineStep<ComputeStepArgs, unknown>({
27
27
  kind: "compute",
28
28
  defaultFailureStrategy: "throw",
29
29
  resultKey: (args) => args.name,
30
- run: (args, ctx) => args.fn(ctx),
30
+ run: (args, ctx) => args.fn(ctx), // @wrapper-known semantic-alias
31
31
  });
32
32
 
33
33
  export function buildComputeStep<TResult>(
@@ -23,6 +23,7 @@ defineStep<ReturnStepArgs, WriteResult<unknown>>({
23
23
  kind: "return",
24
24
  defaultFailureStrategy: "throw",
25
25
  resultKey: () => RETURN_RESULT_KEY,
26
+ // @wrapper-known semantic-alias
26
27
  run: (args, ctx: PipelineCtx) => resolveRequired(args.resolver, ctx),
27
28
  });
28
29
 
@@ -66,6 +66,13 @@ export type ConfigComputedFn<T extends ConfigKeyType = ConfigKeyType> = (
66
66
  ctx: ConfigComputedContext,
67
67
  ) => Promise<ConfigValue<T>>;
68
68
 
69
+ // Storage-Backing eines provisionierten Config-Keys. "config" (Default) =
70
+ // config_values-Projektion mit voller Cascade (user→tenant→system→app→default).
71
+ // "secrets" = read_tenant_secrets (flach pro (tenant,key), AES-GCM-Envelope mit
72
+ // Rotation/Audit, KEINE Cascade) — nur sinnvoll für scope:system ohne
73
+ // Tenant-Override. Die backing×scope-Matrix erzwingt der boot-validator.
74
+ export type ConfigBacking = "config" | "secrets";
75
+
69
76
  export type ConfigKeyDefinition<T extends ConfigKeyType = ConfigKeyType> = {
70
77
  readonly type: T;
71
78
  readonly default?: ConfigValue<T>;
@@ -91,6 +98,34 @@ export type ConfigKeyDefinition<T extends ConfigKeyType = ConfigKeyType> = {
91
98
  // config:query:readiness; keep in sync with the feature's requireNonEmpty
92
99
  // calls in its build-fn.
93
100
  readonly required?: boolean;
101
+
102
+ // --- Provisioning-Metadata (optional auf createTenant/System/UserConfig) ---
103
+ // ENV-Var-Name, dessen Wert beim Boot als app-override-Default dieses Keys
104
+ // gebrückt wird. Reiner Fallback — überschreibt keinen gesetzten Row.
105
+ readonly env?: string;
106
+ // false → der geerbte system-row-Wert wird für Tenant-Admins redigiert
107
+ // (cascade.query + values.query): der Tenant sieht weder den Wert noch dass
108
+ // er gesetzt ist, nur den eigenen Override. Greift quell-basiert auf jeden
109
+ // Wert aus der system-row — nicht scope-gebunden; typischer Fall ist ein
110
+ // scope:tenant Key, dessen Plattform-Default in der system-row liegt (SMTP-
111
+ // Creds). Default true = transparente Cascade.
112
+ readonly inheritedToTenant?: boolean;
113
+ // "config" (Default, volle Cascade) oder "secrets" (flach pro (tenant,key)).
114
+ readonly backing?: ConfigBacking;
115
+ // Markiert den Key als user-facing Einstellung: der Self-Populating
116
+ // Settings-Hub leitet daraus automatisch Screen+Nav-Eintrag ab (kein
117
+ // manuelles r.screen/r.nav). Fehlt `mask`, gilt der Key als internes
118
+ // Plumbing (ENV-provisioniert/computed) und erscheint NICHT im Hub.
119
+ readonly mask?: ConfigMask;
120
+ };
121
+
122
+ // Label-Träger für den Settings-Hub. `title` ist ein i18n-Key (kein Literal —
123
+ // Guard), `icon` ein Icon-Registry-Key für den Nav-Eintrag, `order` die
124
+ // Sortier-Gewichtung innerhalb seiner Audience-Gruppe.
125
+ export type ConfigMask = {
126
+ readonly title: string;
127
+ readonly icon?: string;
128
+ readonly order?: number;
94
129
  };
95
130
 
96
131
  export type ConfigDefinition = {
@@ -12,6 +12,7 @@ export type {
12
12
  export type {
13
13
  ConfigAccessor,
14
14
  ConfigAccessorFactory,
15
+ ConfigBacking,
15
16
  ConfigBounds,
16
17
  ConfigCascade,
17
18
  ConfigCascadeLevel,
@@ -22,6 +23,7 @@ export type {
22
23
  ConfigKeyDefinition,
23
24
  ConfigKeyHandle,
24
25
  ConfigKeyType,
26
+ ConfigMask,
25
27
  ConfigResolver,
26
28
  ConfigSeedDef,
27
29
  ConfigStoredRow,
@@ -344,6 +344,13 @@ export type EntityEditScreenDefinition = {
344
344
  * (History-/Audit-Erhalt): unterdrückt den Löschen-Button im
345
345
  * Update-Form. */
346
346
  readonly allowDelete?: boolean;
347
+ /** Optionaler per-Field-Label-i18n-Key (Field-Name → Key), überschreibt
348
+ * die Default-Konvention `<feature>:entity:<entity>:field:<name>`.
349
+ * Primär für configEdit: dessen Pseudo-Entity `__config-edit__` hat
350
+ * keinen natürlichen Field-Namespace — der Settings-Hub injiziert hier
351
+ * das `mask.title`-Label des Config-Keys. Fehlt ein Eintrag, gilt die
352
+ * Konvention. */
353
+ readonly fieldLabels?: Readonly<Record<string, string>>;
347
354
  readonly slots?: ScreenSlots;
348
355
  readonly access?: AccessRule;
349
356
  };
@@ -470,6 +477,12 @@ export type ConfigEditScreenDefinition = {
470
477
  /** Layout: Sections mit Field-Refs. Identisch zu entityEdit/
471
478
  * actionForm. */
472
479
  readonly layout: EditLayout;
480
+ /** Optionaler per-Field-Label-i18n-Key (Field-Name → Key). Der
481
+ * Settings-Hub setzt hier `mask.title` des jeweiligen Config-Keys,
482
+ * damit das am Key deklarierte Label am generierten Feld erscheint —
483
+ * ohne es unter der `__config-edit__`-Konvention zu duplizieren.
484
+ * Fehlt ein Eintrag, gilt die Konvention. */
485
+ readonly fieldLabels?: Readonly<Record<string, string>>;
473
486
  /** i18n-key für den Submit-Button. Default: "kumiko.actions.save". */
474
487
  readonly submitLabel?: string;
475
488
  readonly slots?: ScreenSlots;
package/src/env/index.ts CHANGED
@@ -106,6 +106,7 @@ export function getDefaultValue(field: z.ZodType): unknown {
106
106
  return undefined;
107
107
  }
108
108
 
109
+ // @wrapper-known semantic-alias
109
110
  export function getFieldDescription(field: z.ZodType): string | undefined {
110
111
  return zodDescription(field);
111
112
  }
@@ -37,10 +37,12 @@ export function writeFailure(err: KumikoError): WriteFailure {
37
37
  // (typed 404) and "business rule violated: REASON" (typed 422 with the reason
38
38
  // string surfaced in details.reason). Reach for the concrete classes when you
39
39
  // need richer payload — these two cover the bulk of handler code.
40
+ // @wrapper-known error-helper
40
41
  export function failNotFound(entity: string, id?: number | string): WriteFailure {
41
42
  return writeFailure(new NotFoundError(entity, id));
42
43
  }
43
44
 
45
+ // @wrapper-known error-helper
44
46
  export function failUnprocessable(
45
47
  reason: string,
46
48
  details?: Readonly<Record<string, unknown>>,