@cosmicdrift/kumiko-framework 0.336.1 → 0.338.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 (116) hide show
  1. package/dist/api/api-constants.d.ts +1 -0
  2. package/dist/api/api-constants.js +9 -0
  3. package/dist/api/auth-middleware.js +45 -21
  4. package/dist/api/auth-routes.d.ts +1 -0
  5. package/dist/api/auth-routes.js +20 -0
  6. package/dist/api/redis-sse-broker.d.ts +1 -0
  7. package/dist/api/redis-sse-broker.js +4 -5
  8. package/dist/api/server.js +14 -4
  9. package/dist/api/sse-route.js +14 -1
  10. package/dist/bun-db/query.js +6 -4
  11. package/dist/compliance/sub-processors.js +10 -1
  12. package/dist/crypto/kek-source.js +1 -0
  13. package/dist/db/blind-index-cleanup.d.ts +1 -0
  14. package/dist/db/blind-index-cleanup.js +12 -1
  15. package/dist/db/event-store-executor-read.js +30 -4
  16. package/dist/db/index.d.ts +1 -1
  17. package/dist/db/index.js +1 -1
  18. package/dist/db/queries/event-consumer.d.ts +2 -2
  19. package/dist/db/queries/event-consumer.js +5 -4
  20. package/dist/db/queries/event-store.js +6 -6
  21. package/dist/db/queries/projection-rebuild.js +4 -4
  22. package/dist/db/queries/seed-context.js +6 -6
  23. package/dist/db/tenant-db.js +16 -8
  24. package/dist/derivatives/derivatives-context.js +5 -2
  25. package/dist/engine/boot-validator/access-declarations.js +13 -7
  26. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  27. package/dist/engine/boot-validator/entity-handler.js +26 -0
  28. package/dist/engine/boot-validator/index.js +2 -1
  29. package/dist/engine/boot-validator/pii-retention.js +11 -1
  30. package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
  31. package/dist/engine/boot-validator/projection-list-screens.js +13 -0
  32. package/dist/engine/boot-validator/screens.js +12 -0
  33. package/dist/engine/build-config-feature-schema.js +4 -0
  34. package/dist/engine/config-helpers.d.ts +2 -1
  35. package/dist/engine/config-helpers.js +1 -0
  36. package/dist/engine/define-feature.js +1 -0
  37. package/dist/engine/extensions/escape-hatch-usage.js +3 -1
  38. package/dist/engine/feature-ast/extractors/hooks.js +7 -1
  39. package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
  40. package/dist/engine/feature-ast/extractors/index.js +1 -1
  41. package/dist/engine/feature-ast/extractors/jobs-routes.js +6 -2
  42. package/dist/engine/feature-ast/extractors/round3.d.ts +2 -1
  43. package/dist/engine/feature-ast/extractors/round3.js +11 -0
  44. package/dist/engine/feature-ast/extractors/shared.js +1 -3
  45. package/dist/engine/feature-ast/index.d.ts +1 -1
  46. package/dist/engine/feature-ast/parse.js +3 -1
  47. package/dist/engine/feature-ast/patch.d.ts +3 -0
  48. package/dist/engine/feature-ast/patch.js +8 -11
  49. package/dist/engine/feature-ast/patcher.d.ts +6 -1
  50. package/dist/engine/feature-ast/patcher.js +3 -0
  51. package/dist/engine/feature-ast/pattern-change-schema.d.ts +630 -1
  52. package/dist/engine/feature-ast/pattern-change-schema.js +19 -2
  53. package/dist/engine/feature-ast/patterns.d.ts +8 -2
  54. package/dist/engine/feature-ast/patterns.js +1 -0
  55. package/dist/engine/feature-ast/render.js +9 -0
  56. package/dist/engine/feature-builder-state.d.ts +2 -1
  57. package/dist/engine/feature-builder-state.js +1 -0
  58. package/dist/engine/feature-config-events-jobs.d.ts +4 -1
  59. package/dist/engine/feature-config-events-jobs.js +25 -1
  60. package/dist/engine/feature-manifest.d.ts +1 -0
  61. package/dist/engine/feature-manifest.js +1 -0
  62. package/dist/engine/feature-ui-extensions.js +24 -21
  63. package/dist/engine/index.d.ts +1 -1
  64. package/dist/engine/pattern-library/library.js +2 -1
  65. package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
  66. package/dist/engine/pattern-library/static-schemas.js +47 -4
  67. package/dist/engine/registry-facade.js +7 -0
  68. package/dist/engine/registry-ingest.js +7 -0
  69. package/dist/engine/registry-state.d.ts +2 -1
  70. package/dist/engine/registry-state.js +1 -0
  71. package/dist/engine/registry-validate.d.ts +1 -1
  72. package/dist/engine/registry-validate.js +39 -17
  73. package/dist/engine/registry.js +2 -2
  74. package/dist/engine/types/index.d.ts +3 -3
  75. package/dist/engine/types/index.js +1 -1
  76. package/dist/engine/types/screen.d.ts +1 -1
  77. package/dist/engine/types/screen.js +1 -1
  78. package/dist/errors/classes.d.ts +5 -0
  79. package/dist/errors/classes.js +15 -0
  80. package/dist/errors/index.d.ts +1 -1
  81. package/dist/errors/index.js +1 -1
  82. package/dist/i18n/required-surface-keys.js +3 -5
  83. package/dist/jobs/job-runner.js +2 -1
  84. package/dist/migrations/pending-rebuilds.d.ts +2 -0
  85. package/dist/migrations/pending-rebuilds.js +3 -0
  86. package/dist/pipeline/dispatch-query.js +3 -5
  87. package/dist/pipeline/dispatch-shared.js +32 -5
  88. package/dist/pipeline/dispatch-stream.js +2 -4
  89. package/dist/pipeline/dispatch-write.js +2 -4
  90. package/dist/pipeline/event-dispatcher-delivery.d.ts +0 -1
  91. package/dist/pipeline/event-dispatcher-delivery.js +1 -8
  92. package/dist/pipeline/event-dispatcher.js +16 -25
  93. package/dist/pipeline/handler-access-error.d.ts +3 -0
  94. package/dist/pipeline/handler-access-error.js +17 -0
  95. package/dist/pipeline/irreversible-operation-gate.js +9 -3
  96. package/dist/pipeline/member-read-only-transaction.js +8 -3
  97. package/dist/pipeline/system-hooks.js +7 -1
  98. package/dist/pipeline/system-identity-switch.js +11 -8
  99. package/dist/search/meilisearch-adapter.js +0 -2
  100. package/dist/stack/push-entity-projection-tables.js +5 -3
  101. package/dist/stack/redis.d.ts +1 -0
  102. package/dist/stack/redis.js +11 -2
  103. package/dist/stack/table-helpers.d.ts +3 -1
  104. package/dist/stack/table-helpers.js +9 -7
  105. package/dist/stack/test-stack.js +1 -7
  106. package/dist/testing/handler-context.js +3 -6
  107. package/dist/ui-types/index.d.ts +1 -1
  108. package/dist/ui-types/index.js +1 -1
  109. package/dist/upgrade-cli.js +29 -2
  110. package/package.json +4 -4
  111. package/src/changes.json +153 -0
  112. package/src/scripts/codemod/collect-source-files.ts +48 -0
  113. package/src/scripts/codemod/migrate-cross-tenant.ts +3 -23
  114. package/src/scripts/codemod/migrate-db-raw.ts +6 -23
  115. package/src/scripts/codemod/migrate-open-to-all.ts +4 -24
  116. package/src/scripts/codemod/unprocessable-error-details-reason.ts +47 -3
@@ -21,6 +21,7 @@
21
21
  // resolve this statically" markers) and, for EntityDefinition only,
22
22
  // additionally checked for field-type membership via the shared
23
23
  // `findUnknownEntityFieldTypes` helper (kept in sync with `extractEntity`).
24
+ import { ESCAPE_HATCH_GRANTS } from "@cosmicdrift/kumiko-types/handlers";
24
25
  import * as z from "zod";
25
26
  import { LifecycleHookTypes } from "../constants.js";
26
27
  import { describeUnknownFieldType, findUnknownEntityFieldTypes } from "./entity-field-types.js";
@@ -131,6 +132,7 @@ const escapeHatchSchema = z
131
132
  reason: z.string().refine((s) => s.trim().length > 0, {
132
133
  message: "reason must be non-empty",
133
134
  }),
135
+ grants: z.array(z.enum(ESCAPE_HATCH_GRANTS)).min(1).optional(),
134
136
  })
135
137
  .strict();
136
138
  const rawRefSentinelSchema = z.object({ __raw: z.string() }).strict();
@@ -413,6 +415,14 @@ const secretSchema = z
413
415
  options: passthroughSchema(),
414
416
  })
415
417
  .strict();
418
+ const secretNamespaceSchema = z
419
+ .object({
420
+ kind: z.literal("secretNamespace"),
421
+ source: sourceFieldSchema,
422
+ shortName: z.string(),
423
+ options: passthroughSchema(),
424
+ })
425
+ .strict();
416
426
  const claimKeySchema = z
417
427
  .object({
418
428
  kind: z.literal("claimKey"),
@@ -707,6 +717,7 @@ export const PATTERN_SCHEMAS_BY_KIND = {
707
717
  uiHints: uiHintsSchema,
708
718
  metric: metricSchema,
709
719
  secret: secretSchema,
720
+ secretNamespace: secretNamespaceSchema,
710
721
  claimKey: claimKeySchema,
711
722
  referenceData: referenceDataSchema,
712
723
  readsConfig: readsConfigSchema,
@@ -733,7 +744,7 @@ export const PATTERN_SCHEMAS_BY_KIND = {
733
744
  "ai.classify": aiClassifySchema,
734
745
  unknown: unknownPatternSchema,
735
746
  };
736
- const patternSchema = z.discriminatedUnion("kind", [
747
+ export const patternSchema = z.discriminatedUnion("kind", [
737
748
  entitySchema,
738
749
  relationSchema,
739
750
  navSchema,
@@ -748,6 +759,7 @@ const patternSchema = z.discriminatedUnion("kind", [
748
759
  uiHintsSchema,
749
760
  metricSchema,
750
761
  secretSchema,
762
+ secretNamespaceSchema,
751
763
  claimKeySchema,
752
764
  referenceDataSchema,
753
765
  readsConfigSchema,
@@ -810,6 +822,9 @@ const patternIdMetricSchema = z
810
822
  const patternIdSecretSchema = z
811
823
  .object({ kind: z.literal("secret"), shortName: z.string() })
812
824
  .strict();
825
+ const patternIdSecretNamespaceSchema = z
826
+ .object({ kind: z.literal("secretNamespace"), shortName: z.string() })
827
+ .strict();
813
828
  const patternIdClaimKeySchema = z
814
829
  .object({ kind: z.literal("claimKey"), shortName: z.string() })
815
830
  .strict();
@@ -870,6 +885,7 @@ export const PATTERN_ID_SCHEMAS_BY_KIND = {
870
885
  hook: patternIdHookSchema,
871
886
  metric: patternIdMetricSchema,
872
887
  secret: patternIdSecretSchema,
888
+ secretNamespace: patternIdSecretNamespaceSchema,
873
889
  claimKey: patternIdClaimKeySchema,
874
890
  referenceData: patternIdReferenceDataSchema,
875
891
  useExtension: patternIdUseExtensionSchema,
@@ -895,7 +911,7 @@ export const PATTERN_ID_SCHEMAS_BY_KIND = {
895
911
  authClaims: patternIdAuthClaimsSchema,
896
912
  treeActions: patternIdTreeActionsSchema,
897
913
  };
898
- const patternIdSchema = z.discriminatedUnion("kind", [
914
+ export const patternIdSchema = z.discriminatedUnion("kind", [
899
915
  patternIdEntitySchema,
900
916
  patternIdRelationSchema,
901
917
  patternIdNavSchema,
@@ -907,6 +923,7 @@ const patternIdSchema = z.discriminatedUnion("kind", [
907
923
  patternIdHookSchema,
908
924
  patternIdMetricSchema,
909
925
  patternIdSecretSchema,
926
+ patternIdSecretNamespaceSchema,
910
927
  patternIdClaimKeySchema,
911
928
  patternIdReferenceDataSchema,
912
929
  patternIdUseExtensionSchema,
@@ -1,6 +1,6 @@
1
1
  import type { LifecycleHookType } from "../constants.js";
2
2
  import type { ConfigKeyDefinition, ConfigKeyType, JobDefinition, ReferenceDataDef, RunIn, TranslationKeys } from "../types/config.js";
3
- import type { MetricOptions, SecretOptions } from "../types/feature.js";
3
+ import type { MetricOptions, SecretNamespaceOptions, SecretOptions } from "../types/feature.js";
4
4
  import type { EntityDefinition } from "../types/fields.js";
5
5
  import type { AccessRule, AgentHandlerHints, ClaimKeyType, EscapeHatchDeclaration, RateLimitDeclaration } from "../types/handlers.js";
6
6
  import type { HookPhase } from "../types/hooks.js";
@@ -86,6 +86,12 @@ export type SecretPattern = {
86
86
  readonly shortName: string;
87
87
  readonly options: SecretOptions;
88
88
  };
89
+ export type SecretNamespacePattern = {
90
+ readonly kind: "secretNamespace";
91
+ readonly source: SourceLocation;
92
+ readonly shortName: string;
93
+ readonly options: SecretNamespaceOptions;
94
+ };
89
95
  export type ClaimKeyPattern = {
90
96
  readonly kind: "claimKey";
91
97
  readonly source: SourceLocation;
@@ -295,7 +301,7 @@ export type UnknownPattern = {
295
301
  readonly source: SourceLocation;
296
302
  readonly methodName: string;
297
303
  };
298
- export type FeaturePattern = EntityPattern | RelationPattern | NavPattern | WorkspacePattern | ConfigPattern | TranslationsPattern | RequiresPattern | OptionalRequiresPattern | SystemScopePattern | ToggleablePattern | DescribePattern | UiHintsPattern | MetricPattern | SecretPattern | ClaimKeyPattern | ReferenceDataPattern | ReadsConfigPattern | UseExtensionPattern | UsesApiPattern | ExposesApiPattern | TreeActionsPattern | ScreenPattern | WriteHandlerPattern | QueryHandlerPattern | StreamHandlerPattern | HookPattern | JobPattern | NotificationPattern | AuthClaimsPattern | HttpRoutePattern | ProjectionPattern | MultiStreamProjectionPattern | DefineEventPattern | ExtendsRegistrarPattern | EnvSchemaPattern | AiGeneratePattern | AiExtractPattern | AiClassifyPattern | UnknownPattern;
304
+ export type FeaturePattern = EntityPattern | RelationPattern | NavPattern | WorkspacePattern | ConfigPattern | TranslationsPattern | RequiresPattern | OptionalRequiresPattern | SystemScopePattern | ToggleablePattern | DescribePattern | UiHintsPattern | MetricPattern | SecretPattern | SecretNamespacePattern | ClaimKeyPattern | ReferenceDataPattern | ReadsConfigPattern | UseExtensionPattern | UsesApiPattern | ExposesApiPattern | TreeActionsPattern | ScreenPattern | WriteHandlerPattern | QueryHandlerPattern | StreamHandlerPattern | HookPattern | JobPattern | NotificationPattern | AuthClaimsPattern | HttpRoutePattern | ProjectionPattern | MultiStreamProjectionPattern | DefineEventPattern | ExtendsRegistrarPattern | EnvSchemaPattern | AiGeneratePattern | AiExtractPattern | AiClassifyPattern | UnknownPattern;
299
305
  export type FeaturePatternKind = FeaturePattern["kind"];
300
306
  export type Editability = "static" | "mixed" | "opaque";
301
307
  export declare function getEditability(pattern: FeaturePattern): Editability;
@@ -74,6 +74,7 @@ export function getEditability(pattern) {
74
74
  case "describe":
75
75
  case "metric":
76
76
  case "secret":
77
+ case "secretNamespace":
77
78
  case "claimKey":
78
79
  case "referenceData":
79
80
  case "readsConfig":
@@ -61,6 +61,8 @@ export function renderPattern(pattern) {
61
61
  return renderMetric(pattern);
62
62
  case "secret":
63
63
  return renderSecret(pattern);
64
+ case "secretNamespace":
65
+ return renderSecretNamespace(pattern);
64
66
  case "claimKey":
65
67
  return renderClaimKey(pattern);
66
68
  case "referenceData":
@@ -245,6 +247,13 @@ function renderSecret(p) {
245
247
  const merged = { name: p.shortName, ...p.options };
246
248
  return `r.secret(${renderValue(merged)});`;
247
249
  }
250
+ function renderSecretNamespace(p) {
251
+ if (isRawRefSentinel(p.options)) {
252
+ return `r.secretNamespace(${JSON.stringify(p.shortName)}, ${p.options.__raw});`;
253
+ }
254
+ const merged = { name: p.shortName, ...p.options };
255
+ return `r.secretNamespace(${renderValue(merged)});`;
256
+ }
248
257
  function renderClaimKey(p) {
249
258
  return `r.claimKey(${renderValue({ name: p.shortName, type: p.claimType })});`;
250
259
  }
@@ -2,7 +2,7 @@ import type { WebSocketRouteDefinition } from "@cosmicdrift/kumiko-types/websock
2
2
  import type * as z from "zod";
3
3
  import type { ZodType } from "zod";
4
4
  import type { HttpRouteDefinition } from "./types/http-route.js";
5
- import type { AuthClaimsFn, BootCheckFn, ClaimKeyDefinition, ConfigKeyDefinition, ConfigSeedDef, EntityDefinition, EntityProjectionExtension, EventMigrationDef, ExtensionSelectorDef, FeatureMetricDef, JobDefinition, LifecycleHookFn, MultiStreamProjectionDefinition, NotificationDefinition, OwnedFn, PhasedHook, PostDeleteHookFn, PostQueryHookFn, PostSaveHookFn, PreDeleteHookFn, ProjectionDefinition, QueryHandlerDef, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionRegistration, RelationDefinition, SearchPayloadContributorFn, SecretKeyDefinition, StoreTableEntry, StreamHandlerDef, TranslationKeys, TreeActionDef, UiHints, ValidationHookFn, WriteHandlerDef } from "./types/index.js";
5
+ import type { AuthClaimsFn, BootCheckFn, ClaimKeyDefinition, ConfigKeyDefinition, ConfigSeedDef, EntityDefinition, EntityProjectionExtension, EventMigrationDef, ExtensionSelectorDef, FeatureMetricDef, JobDefinition, LifecycleHookFn, MultiStreamProjectionDefinition, NotificationDefinition, OwnedFn, PhasedHook, PostDeleteHookFn, PostQueryHookFn, PostSaveHookFn, PreDeleteHookFn, ProjectionDefinition, QueryHandlerDef, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionRegistration, RelationDefinition, SearchPayloadContributorFn, SecretKeyDefinition, SecretNamespaceDefinition, StoreTableEntry, StreamHandlerDef, TranslationKeys, TreeActionDef, UiHints, ValidationHookFn, WriteHandlerDef } from "./types/index.js";
6
6
  import type { ContentCollectionDefinition, NavDefinition } from "./types/nav.js";
7
7
  import type { ScreenDefinition } from "./types/screen.js";
8
8
  import type { WorkspaceDefinition } from "./types/workspace.js";
@@ -46,6 +46,7 @@ export type FeatureBuilderState = {
46
46
  handlerEntityMappings: Record<string, string>;
47
47
  metrics: Record<string, FeatureMetricDef>;
48
48
  secretKeys: Record<string, SecretKeyDefinition>;
49
+ secretNamespaces: Record<string, SecretNamespaceDefinition>;
49
50
  projections: Record<string, ProjectionDefinition>;
50
51
  multiStreamProjections: Record<string, MultiStreamProjectionDefinition>;
51
52
  entityProjectionExtensions: Record<string, EntityProjectionExtension[]>;
@@ -41,6 +41,7 @@ export function createInitialFeatureBuilderState() {
41
41
  handlerEntityMappings: {},
42
42
  metrics: {},
43
43
  secretKeys: {},
44
+ secretNamespaces: {},
44
45
  projections: {},
45
46
  multiStreamProjections: {},
46
47
  entityProjectionExtensions: {},
@@ -1,7 +1,7 @@
1
1
  import type * as z from "zod";
2
2
  import { type ZodType } from "zod";
3
3
  import type { FeatureBuilderState } from "./feature-builder-state.js";
4
- import type { AuthClaimsFn, ClaimKeyHandle, ClaimKeyType, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigSeedDef, DeclarativeEventMigration, EventDef, EventPiiStance, EventUpcastFn, JobDefinition, JobHandlerFn, MetricOptions, NameOrRef, NotificationDataFn, NotificationRecipientFn, NotificationTemplateFn, QualifiedEventName, SecretKeyHandle, SecretOptions, TranslationsDef } from "./types/index.js";
4
+ import type { AuthClaimsFn, ClaimKeyHandle, ClaimKeyType, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigSeedDef, DeclarativeEventMigration, EventDef, EventPiiStance, EventUpcastFn, JobDefinition, JobHandlerFn, MetricOptions, NameOrRef, NotificationDataFn, NotificationRecipientFn, NotificationTemplateFn, QualifiedEventName, SecretKeyHandle, SecretNamespaceHandle, SecretNamespaceOptions, SecretOptions, TranslationsDef } from "./types/index.js";
5
5
  export declare function buildConfigEventsJobsMethods<TName extends string>(state: FeatureBuilderState, name: TName): {
6
6
  config: {
7
7
  <T extends ConfigKeyType>(keyName: string, def: ConfigKeyDefinition<T>): ConfigKeyHandle<T>;
@@ -48,6 +48,9 @@ export declare function buildConfigEventsJobsMethods<TName extends string>(state
48
48
  secret(shortNameOrDefinition: string | ({
49
49
  readonly name: string;
50
50
  } & SecretOptions), options?: SecretOptions): SecretKeyHandle;
51
+ secretNamespace(shortNameOrDefinition: string | ({
52
+ readonly name: string;
53
+ } & SecretNamespaceOptions), options?: SecretNamespaceOptions): SecretNamespaceHandle;
51
54
  claimKey<T extends ClaimKeyType>(shortNameOrDefinition: string | {
52
55
  readonly name: string;
53
56
  readonly type: T;
@@ -1,4 +1,4 @@
1
- import { normalizeEventPiiSubject } from "@cosmicdrift/kumiko-types/handlers";
1
+ import { escapeHatchGrantsProblem, normalizeEventPiiSubject, } from "@cosmicdrift/kumiko-types/handlers";
2
2
  import { ZodObject } from "zod";
3
3
  import { resolveName } from "./handler-helpers.js";
4
4
  import { splitNamedDefinition, unwrapArrayForm } from "./object-form.js";
@@ -124,6 +124,10 @@ export function buildConfigEventsJobsMethods(state, name) {
124
124
  throw new Error(`[Feature ${name}] r.job("${jobName}") declares { escapeHatch: { reason: "" } } — ` +
125
125
  "the reason must be a non-empty string explaining why this job needs unfiltered database access.");
126
126
  }
127
+ const jobGrantsProblem = jobOptions.escapeHatch && escapeHatchGrantsProblem(jobOptions.escapeHatch);
128
+ if (jobGrantsProblem) {
129
+ throw new Error(`[Feature ${name}] r.job("${jobName}") declares an invalid escapeHatch — ${jobGrantsProblem}`);
130
+ }
127
131
  // Resolve NameOrRef(s) in trigger.on. Multi-Trigger-Form: Array
128
132
  // wird zu Array von resolved strings, Single bleibt single string —
129
133
  // job-runner unterscheidet anhand Array.isArray.
@@ -235,6 +239,10 @@ export function buildConfigEventsJobsMethods(state, name) {
235
239
  throw new Error(`[Feature ${name}] Secret "${shortName}" already registered. ` +
236
240
  `Secret key names must be unique per feature.`);
237
241
  }
242
+ if (secretOptions.writeRoles !== undefined && secretOptions.writeRoles.length === 0) {
243
+ throw new Error(`[Feature ${name}] Secret "${shortName}" declares an empty writeRoles list, ` +
244
+ `which nobody could ever satisfy. Omit writeRoles to fall back to the secrets handler access.`);
245
+ }
238
246
  // Qualified name follows the framework's "<feature>:<type>:<name>"
239
247
  // QN convention — same pattern config / jobs / events use. toKebab
240
248
  // handles the common input shapes ("stripe.apiKey" → "stripe-api-key")
@@ -248,6 +256,22 @@ export function buildConfigEventsJobsMethods(state, name) {
248
256
  };
249
257
  return { name: qualifiedName };
250
258
  },
259
+ secretNamespace(shortNameOrDefinition, options) {
260
+ const [shortName, namespaceOptions] = typeof shortNameOrDefinition === "string"
261
+ ? [shortNameOrDefinition, options]
262
+ : splitNamedDefinition(shortNameOrDefinition);
263
+ if (state.secretNamespaces[shortName]) {
264
+ throw new Error(`[Feature ${name}] Secret namespace "${shortName}" already registered. ` +
265
+ `Secret namespace names must be unique per feature.`);
266
+ }
267
+ if (namespaceOptions.writeRoles !== undefined && namespaceOptions.writeRoles.length === 0) {
268
+ throw new Error(`[Feature ${name}] Secret namespace "${shortName}" declares an empty writeRoles list, ` +
269
+ `which nobody could ever satisfy. Omit writeRoles to fall back to the secrets handler access.`);
270
+ }
271
+ const qualifiedPrefix = `${toKebab(name)}:${toKebab(shortName)}.`;
272
+ state.secretNamespaces[shortName] = { shortName, qualifiedPrefix, ...namespaceOptions };
273
+ return { prefix: qualifiedPrefix, keyFor: (suffix) => `${qualifiedPrefix}${suffix}` };
274
+ },
251
275
  claimKey(shortNameOrDefinition, options) {
252
276
  const shortName = typeof shortNameOrDefinition === "string"
253
277
  ? shortNameOrDefinition
@@ -7,6 +7,7 @@ export type ManifestConfigKey = {
7
7
  readonly default: string | number | boolean | null;
8
8
  readonly encrypted: boolean;
9
9
  readonly computed: boolean;
10
+ readonly validated: boolean;
10
11
  readonly options: readonly string[] | null;
11
12
  readonly bounds: {
12
13
  readonly min?: number;
@@ -29,6 +29,7 @@ export function buildManifestFromRegistry(registry, options) {
29
29
  default: def.default ?? null,
30
30
  encrypted: isEncryptedAtRest(def),
31
31
  computed: def.computed !== undefined,
32
+ validated: def.validate !== undefined,
32
33
  options: def.options ?? null,
33
34
  bounds: def.bounds ?? null,
34
35
  pattern: def.pattern ?? null,
@@ -1,3 +1,4 @@
1
+ import { escapeHatchGrantsProblem } from "@cosmicdrift/kumiko-types/handlers";
1
2
  import { WEBSOCKET_MAX_CONNECTIONS_PER_USER_LIMIT, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_ROUTE_PATH_PREFIX, } from "@cosmicdrift/kumiko-types/websocket-route";
2
3
  import { bindHookEscapeHatchGrant } from "../pipeline/system-identity-switch.js";
3
4
  import { LifecycleHookTypes } from "./constants.js";
@@ -99,7 +100,10 @@ export function buildUiExtensionsMethods(state, name) {
99
100
  `Nav ids must be unique per feature — remove the standalone ` +
100
101
  `r.nav("${navDefinition.id}", ...) call or the screen's inline nav.`);
101
102
  }
102
- state.navs[navDefinition.id] = navDefinition;
103
+ // Explicit `undefined` values (e.g. `icon: cond ? "x" : undefined`) would trip
104
+ // buildAppSchema's JSON-safety check, so strip them for every nav path.
105
+ // @cast-boundary engine-bridge — filtering undefined keeps the NavDefinition shape.
106
+ state.navs[navDefinition.id] = Object.fromEntries(Object.entries(navDefinition).filter(([, value]) => value !== undefined));
103
107
  }
104
108
  return {
105
109
  hook(type, target, fn, options) {
@@ -114,6 +118,10 @@ export function buildUiExtensionsMethods(state, name) {
114
118
  throw new Error(`[Feature ${name}] r.hook("${type}", ...) declares { escapeHatch: { reason: "" } } — ` +
115
119
  "the reason must be a non-empty string explaining why this hook switches identity to SYSTEM.");
116
120
  }
121
+ const hookGrantsProblem = options?.escapeHatch && escapeHatchGrantsProblem(options.escapeHatch);
122
+ if (hookGrantsProblem) {
123
+ throw new Error(`[Feature ${name}] r.hook("${type}", ...) declares an invalid escapeHatch — ${hookGrantsProblem}`);
124
+ }
117
125
  // Wrapped once for both branches below; validation hooks stay unwrapped (no context).
118
126
  const hookLabel = `${type} hook of feature "${name}"`;
119
127
  const wrapped = type === "validation"
@@ -208,6 +216,10 @@ export function buildUiExtensionsMethods(state, name) {
208
216
  throw new Error(`[Feature ${name}] r.useExtension("${extensionName}", "${resolvedEntityName}", ...) declares an invalid { escapeHatch } — ` +
209
217
  `must be { reason: "<non-empty string>" } explaining why this usage needs unfiltered db access.`);
210
218
  }
219
+ const extGrantsProblem = escapeHatchGrantsProblem(escapeHatch);
220
+ if (extGrantsProblem) {
221
+ throw new Error(`[Feature ${name}] r.useExtension("${extensionName}", "${resolvedEntityName}", ...) declares an invalid { escapeHatch } — ${extGrantsProblem}`);
222
+ }
211
223
  }
212
224
  state.extensionUsages.push({
213
225
  extensionName,
@@ -337,15 +349,12 @@ export function buildUiExtensionsMethods(state, name) {
337
349
  // Sugar for the common "one nav entry pointing at this screen"
338
350
  // case — synthesizes id/screen from the screen's own id. Beyond
339
351
  // label/icon/parent/order, declare a standalone r.nav() instead.
340
- // Optional fields stay absent rather than explicitly undefined —
341
- // buildAppSchema's JSON-safety check flags undefined values. Same
342
- // pattern as r.contentCollection() below.
343
352
  registerNav({
344
353
  id: definition.id,
345
354
  label: definition.nav.label,
346
- ...(definition.nav.icon !== undefined && { icon: definition.nav.icon }),
347
- ...(definition.nav.parent !== undefined && { parent: definition.nav.parent }),
348
- ...(definition.nav.order !== undefined && { order: definition.nav.order }),
355
+ icon: definition.nav.icon,
356
+ parent: definition.nav.parent,
357
+ order: definition.nav.order,
349
358
  screen: `${name}:screen:${definition.id}`,
350
359
  });
351
360
  }
@@ -359,26 +368,20 @@ export function buildUiExtensionsMethods(state, name) {
359
368
  `Collection ids must be unique per feature.`);
360
369
  }
361
370
  // registerNav owns the kebab + collision checks, including collisions
362
- // with a plain r.nav() of the same id. Optional fields stay absent
363
- // rather than explicitly undefined — buildAppSchema's JSON-safety check
364
- // flags undefined values.
371
+ // with a plain r.nav() of the same id.
365
372
  registerNav({
366
373
  id: definition.id,
367
374
  label: definition.nav.label,
368
- ...(definition.nav.icon !== undefined && { icon: definition.nav.icon }),
369
- ...(definition.nav.parent !== undefined && { parent: definition.nav.parent }),
370
- ...(definition.nav.order !== undefined && { order: definition.nav.order }),
375
+ icon: definition.nav.icon,
376
+ parent: definition.nav.parent,
377
+ order: definition.nav.order,
371
378
  // Nav visibility follows the collection's access unless the caller
372
379
  // overrode it — a node the handler would refuse has no business in
373
380
  // the sidebar.
374
- ...((definition.nav.access ?? definition.access) !== undefined && {
375
- access: definition.nav.access ?? definition.access,
376
- }),
377
- ...(definition.nav.workspaces !== undefined && { workspaces: definition.nav.workspaces }),
378
- ...(definition.nav.createAction !== undefined && {
379
- createAction: definition.nav.createAction,
380
- }),
381
- ...(definition.nav.actions !== undefined && { actions: definition.nav.actions }),
381
+ access: definition.nav.access ?? definition.access,
382
+ workspaces: definition.nav.workspaces,
383
+ createAction: definition.nav.createAction,
384
+ actions: definition.nav.actions,
382
385
  // The tree children come from a runtime provider keyed on this QN —
383
386
  // a collection without it would render as an empty leaf.
384
387
  provider: true,
@@ -73,7 +73,7 @@ export { describeWorkflowStepError } from "./steps/describe-workflow-step-error.
73
73
  export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
74
74
  export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
75
75
  export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
76
- export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DateFieldDef, DecimalFieldDef, DeleteContext, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EntityDefinition, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyFactory, NotifyFn, NotifyOptions, NotifyPriority, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
76
+ export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DateFieldDef, DecimalFieldDef, DeleteContext, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EntityDefinition, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyFactory, NotifyFn, NotifyOptions, NotifyPriority, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
77
77
  export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, HookPhases, isAgentVisibleScreen, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
78
78
  export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.js";
79
79
  export { runValidation } from "./validation.js";
@@ -14,7 +14,7 @@
14
14
  // the Designer/LLM relies on.
15
15
  import { aiClassifySchema, aiExtractSchema, aiGenerateSchema, authClaimsSchema, defineEventSchema, hookSchema, httpRouteSchema, jobSchema, multiStreamProjectionSchema, notificationSchema, projectionSchema, queryHandlerSchema, screenSchema, streamHandlerSchema, writeHandlerSchema, } from "./mixed-schemas.js";
16
16
  import { envSchemaSchema, exposesApiSchema, extendsRegistrarSchema, treeActionsSchema, unknownSchema, usesApiSchema, } from "./opaque-schemas.js";
17
- import { claimKeySchema, configSchema, describeSchema, entitySchema, metricSchema, navSchema, optionalRequiresSchema, readsConfigSchema, referenceDataSchema, relationSchema, requiresSchema, secretSchema, systemScopeSchema, toggleableSchema, translationsSchema, uiHintsSchema, useExtensionSchema, workspaceSchema, } from "./static-schemas.js";
17
+ import { claimKeySchema, configSchema, describeSchema, entitySchema, metricSchema, navSchema, optionalRequiresSchema, readsConfigSchema, referenceDataSchema, relationSchema, requiresSchema, secretNamespaceSchema, secretSchema, systemScopeSchema, toggleableSchema, translationsSchema, uiHintsSchema, useExtensionSchema, workspaceSchema, } from "./static-schemas.js";
18
18
  export const PATTERN_LIBRARY = {
19
19
  requires: requiresSchema,
20
20
  optionalRequires: optionalRequiresSchema,
@@ -31,6 +31,7 @@ export const PATTERN_LIBRARY = {
31
31
  translations: translationsSchema,
32
32
  metric: metricSchema,
33
33
  secret: secretSchema,
34
+ secretNamespace: secretNamespaceSchema,
34
35
  claimKey: claimKeySchema,
35
36
  referenceData: referenceDataSchema,
36
37
  useExtension: useExtensionSchema,
@@ -14,6 +14,7 @@ export declare const configSchema: PatternFormSchema;
14
14
  export declare const translationsSchema: PatternFormSchema;
15
15
  export declare const metricSchema: PatternFormSchema;
16
16
  export declare const secretSchema: PatternFormSchema;
17
+ export declare const secretNamespaceSchema: PatternFormSchema;
17
18
  export declare const claimKeySchema: PatternFormSchema;
18
19
  export declare const referenceDataSchema: PatternFormSchema;
19
20
  export declare const useExtensionSchema: PatternFormSchema;
@@ -351,10 +351,53 @@ export const secretSchema = {
351
351
  path: "options.scope",
352
352
  label: { en: "Scope", de: "Geltungsbereich" },
353
353
  input: "select",
354
- options: [
355
- { value: "tenant", label: { en: "Tenant" } },
356
- { value: "system", label: { en: "System" } },
357
- ],
354
+ options: [{ value: "tenant", label: { en: "Tenant" } }],
355
+ },
356
+ {
357
+ path: "options.writeRoles",
358
+ label: { en: "Write roles", de: "Schreibrollen" },
359
+ hint: {
360
+ en: "Roles allowed to set or delete. Leave empty to rely on the secrets handler access alone.",
361
+ de: "Rollen, die setzen oder löschen dürfen. Leer lassen, dann entscheidet allein der Handler-Zugriff.",
362
+ },
363
+ input: "string-list",
364
+ },
365
+ ],
366
+ };
367
+ export const secretNamespaceSchema = {
368
+ kind: "secretNamespace",
369
+ label: { en: "Secret namespace", de: "Secret-Namespace" },
370
+ summary: { en: "Family of tenant-scoped secrets with a runtime-chosen name." },
371
+ category: "meta",
372
+ editability: "static",
373
+ fields: [
374
+ {
375
+ path: "shortName",
376
+ label: { en: "Short name", de: "Kurzname" },
377
+ input: "text",
378
+ required: true,
379
+ },
380
+ {
381
+ path: "options.label",
382
+ label: { en: "UI label (i18n)", de: "UI-Label (i18n)" },
383
+ hint: { en: "{ en: 'Webhook token', de: 'Webhook-Token' }" },
384
+ input: "json-readonly",
385
+ required: true,
386
+ },
387
+ {
388
+ path: "options.scope",
389
+ label: { en: "Scope", de: "Geltungsbereich" },
390
+ input: "select",
391
+ options: [{ value: "tenant", label: { en: "Tenant" } }],
392
+ },
393
+ {
394
+ path: "options.writeRoles",
395
+ label: { en: "Write roles", de: "Schreibrollen" },
396
+ hint: {
397
+ en: "Roles allowed to set or delete. Leave empty to rely on the secrets handler access alone.",
398
+ de: "Rollen, die setzen oder löschen dürfen. Leer lassen, dann entscheidet allein der Handler-Zugriff.",
399
+ },
400
+ input: "string-list",
358
401
  },
359
402
  ],
360
403
  };
@@ -125,6 +125,13 @@ export function buildRegistryFacade(state) {
125
125
  getSecretKey(qualifiedName) {
126
126
  return state.secretKeyMap.get(qualifiedName);
127
127
  },
128
+ findSecretNamespace(key) {
129
+ for (const [prefix, def] of state.secretNamespaceMap) {
130
+ if (key.startsWith(prefix))
131
+ return def;
132
+ }
133
+ return undefined;
134
+ },
128
135
  getConfigKey(qualifiedKey) {
129
136
  return state.configKeyMap.get(qualifiedKey);
130
137
  },
@@ -221,6 +221,13 @@ export function populateMetricsAndSecrets(state, feature) {
221
221
  }
222
222
  state.secretKeyMap.set(def.qualifiedName, def);
223
223
  }
224
+ for (const def of Object.values(feature.secretNamespaces ?? {})) {
225
+ if (state.secretNamespaceMap.has(def.qualifiedPrefix)) {
226
+ throw new Error(`[Kumiko Secrets] Secret namespace "${def.qualifiedPrefix}" registered multiple times. ` +
227
+ "Namespace prefixes must be globally unique across features.");
228
+ }
229
+ state.secretNamespaceMap.set(def.qualifiedPrefix, def);
230
+ }
224
231
  }
225
232
  // Explicit + multi-stream projections (source-entity indexed) + store tables +
226
233
  // unmanaged tables (both cross-feature-uniqueness-by-physical-name guarded).
@@ -1,5 +1,5 @@
1
1
  import { type QnType, qualifyEntityName } from "./qualified-name.js";
2
- import type { AuthClaimsHookDef, ClaimKeyDefinition, ConfigKeyDefinition, ConfigSeedDef, EntityDefinition, EntityProjectionExtension, EventDef, EventUpcastFn, ExtensionSelectorPanel, FeatureDefinition, FeatureMetricDef, HookPhase, JobDefinition, MultiStreamProjectionDefinition, NavDefinition, NotificationDefinition, OwnedFn, PhasedHook, PostDeleteHookFn, PostQueryHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, QueryHandlerDef, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionRegistration, RelationDefinition, ScreenDefinition, SearchPayloadContributorFn, SecretKeyDefinition, StoreTableDef, StreamHandlerDef, TranslationValue, TreeActionDef, WorkspaceDefinition, WriteHandlerDef } from "./types/index.js";
2
+ import type { AuthClaimsHookDef, ClaimKeyDefinition, ConfigKeyDefinition, ConfigSeedDef, EntityDefinition, EntityProjectionExtension, EventDef, EventUpcastFn, ExtensionSelectorPanel, FeatureDefinition, FeatureMetricDef, HookPhase, JobDefinition, MultiStreamProjectionDefinition, NavDefinition, NotificationDefinition, OwnedFn, PhasedHook, PostDeleteHookFn, PostQueryHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, QueryHandlerDef, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionRegistration, RelationDefinition, ScreenDefinition, SearchPayloadContributorFn, SecretKeyDefinition, SecretNamespaceDefinition, StoreTableDef, StreamHandlerDef, TranslationValue, TreeActionDef, WorkspaceDefinition, WriteHandlerDef } from "./types/index.js";
3
3
  export type IncomingRelation = {
4
4
  sourceEntity: string;
5
5
  relationName: string;
@@ -54,6 +54,7 @@ export type RegistryState = {
54
54
  readonly featureName: string;
55
55
  }>;
56
56
  secretKeyMap: Map<string, SecretKeyDefinition>;
57
+ secretNamespaceMap: Map<string, SecretNamespaceDefinition>;
57
58
  projectionMap: Map<string, ProjectionDefinition>;
58
59
  projectionsBySource: Map<string, ProjectionDefinition[]>;
59
60
  multiStreamProjectionMap: Map<string, MultiStreamProjectionDefinition>;
@@ -122,6 +122,7 @@ export function createInitialState() {
122
122
  mergedTranslations: {},
123
123
  metricMap: new Map(),
124
124
  secretKeyMap: new Map(),
125
+ secretNamespaceMap: new Map(),
125
126
  projectionMap: new Map(),
126
127
  projectionsBySource: new Map(),
127
128
  multiStreamProjectionMap: new Map(),
@@ -16,7 +16,7 @@ export declare function validateRequiredFeatures(state: RegistryState, features:
16
16
  export declare function resolveNotificationTriggersAndRegisterHooks(state: RegistryState): void;
17
17
  export declare function validateLifecycleHookTargets(state: RegistryState): void;
18
18
  export declare function validateEntityHookTargets(state: RegistryState, features: readonly FeatureDefinition[]): void;
19
- export declare function validateJobTriggers(state: RegistryState): void;
19
+ export declare function resolveJobTriggers(state: RegistryState): void;
20
20
  export declare function validateLiveEntities(state: RegistryState): void;
21
21
  export declare function validateBootGates(state: RegistryState): void;
22
22
  export declare function validateJobBackoff(state: RegistryState): void;
@@ -544,29 +544,51 @@ export function validateEntityHookTargets(state, features) {
544
544
  }
545
545
  }
546
546
  }
547
- export function validateJobTriggers(state) {
548
- // Validate: job event triggers must reference an existing write/query
549
- // handler OR an existing r.defineEvent registration. The latter is
550
- // delivered async via the job-trigger event-consumer (server.ts), not
551
- // the synchronous write-handler dispatch path — see
552
- // createJobTriggerEventConsumer in pipeline/system-hooks.ts.
553
- // Multi-Trigger-Form: jeden Eintrag im Array gegen allHandlers prüfen,
554
- // auch wenn nur einer fehlt fail-fast.
547
+ export function resolveJobTriggers(state) {
548
+ // Job event triggers must reference an existing write/query handler OR an
549
+ // existing r.defineEvent registration. The latter is delivered async via the
550
+ // job-trigger event-consumer (server.ts), not the synchronous write-handler
551
+ // dispatch path — see createJobTriggerEventConsumer in
552
+ // pipeline/system-hooks.ts. Short names are qualified against the job's own
553
+ // feature and written back, because the job runner matches by exact QN.
555
554
  const allHandlers = allHandlerQns(state);
556
555
  for (const [jobName, jobDef] of state.jobMap) {
557
556
  if (!("on" in jobDef.trigger))
558
557
  continue;
558
+ const featureName = state.jobFeatureMap.get(jobName);
559
559
  const triggerOn = jobDef.trigger.on;
560
- const triggers = Array.isArray(triggerOn) ? triggerOn : [triggerOn];
561
- for (const t of triggers) {
562
- const rawName = resolveName(t);
563
- if (allHandlers.has(rawName))
564
- continue;
565
- if (state.eventMap.has(rawName))
566
- continue;
567
- throw new Error(`Job "${jobName}" triggers on "${rawName}" but no handler or event with that name exists`);
568
- }
560
+ const resolveTrigger = (trigger) => resolveJobTriggerName(state, allHandlers, jobName, featureName, resolveName(trigger));
561
+ const resolvedOn = isNameOrRefList(triggerOn)
562
+ ? triggerOn.map(resolveTrigger)
563
+ : resolveTrigger(triggerOn);
564
+ state.jobMap.set(jobName, { ...jobDef, trigger: { ...jobDef.trigger, on: resolvedOn } });
565
+ }
566
+ }
567
+ // Array.isArray does not narrow a readonly array out of a union.
568
+ function isNameOrRefList(on) {
569
+ return Array.isArray(on);
570
+ }
571
+ // An already-qualified name (e.g. a misspelled own-feature QN) is not a
572
+ // short name; qualify() rejects it, and the "no handler" error below names it.
573
+ function qualifyShortTriggerName(featureName, type, trigger) {
574
+ try {
575
+ return [qualify(featureName, type, trigger)];
569
576
  }
577
+ catch {
578
+ return [];
579
+ }
580
+ }
581
+ function resolveJobTriggerName(state, allHandlers, jobName, featureName, trigger) {
582
+ if (allHandlers.has(trigger) || state.eventMap.has(trigger))
583
+ return trigger;
584
+ const candidates = featureName === undefined
585
+ ? []
586
+ : ["write", "query", "event"].flatMap((type) => qualifyShortTriggerName(featureName, type, trigger));
587
+ const match = candidates.find((qn) => allHandlers.has(qn) || state.eventMap.has(qn));
588
+ if (match !== undefined)
589
+ return match;
590
+ throw new Error(`Job "${jobName}" triggers on "${trigger}" but no handler or event with that name exists. ` +
591
+ `Tried: ${[trigger, ...candidates].map((name) => `"${name}"`).join(", ")}`);
570
592
  }
571
593
  export function validateLiveEntities(state) {
572
594
  for (const [handlerQn, handler] of state.queryHandlerMap) {