@cosmicdrift/kumiko-framework 0.334.0 → 0.336.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 (178) hide show
  1. package/dist/api/anonymous-live-entities.d.ts +2 -0
  2. package/dist/api/anonymous-live-entities.js +17 -0
  3. package/dist/api/auth-routes.js +19 -4
  4. package/dist/api/client-ip.js +3 -6
  5. package/dist/api/extra-route.js +3 -2
  6. package/dist/api/index.d.ts +2 -1
  7. package/dist/api/index.js +1 -0
  8. package/dist/api/pii-leak-guard.js +7 -4
  9. package/dist/api/redis-sse-broker.d.ts +2 -0
  10. package/dist/api/redis-sse-broker.js +6 -7
  11. package/dist/api/routes.js +16 -17
  12. package/dist/api/server.js +20 -10
  13. package/dist/api/sse-broker.d.ts +3 -0
  14. package/dist/api/sse-broker.js +14 -0
  15. package/dist/api/sse-route.d.ts +5 -2
  16. package/dist/api/sse-route.js +71 -11
  17. package/dist/bun-db/connection.js +0 -2
  18. package/dist/bun-db/index.d.ts +1 -1
  19. package/dist/bun-db/index.js +1 -1
  20. package/dist/bun-db/query.d.ts +1 -0
  21. package/dist/bun-db/query.js +15 -4
  22. package/dist/crypto/kek-source.js +40 -36
  23. package/dist/crypto/kms-adapter.js +2 -2
  24. package/dist/db/apply-entity-event.js +8 -2
  25. package/dist/db/blind-index-cleanup.js +32 -11
  26. package/dist/db/entity-table-meta.js +5 -3
  27. package/dist/db/event-store-executor-context.js +13 -19
  28. package/dist/db/event-store-executor-read.js +45 -38
  29. package/dist/db/event-store-executor-write.js +15 -17
  30. package/dist/db/migrate-generator.js +27 -28
  31. package/dist/db/pg-error.js +9 -5
  32. package/dist/db/queries/backfill-pii.js +15 -5
  33. package/dist/db/queries/shadow-swap.js +5 -4
  34. package/dist/db/table-builder.d.ts +1 -0
  35. package/dist/db/table-builder.js +7 -15
  36. package/dist/db/tenant-db.js +18 -2
  37. package/dist/derivatives/variant-key.js +3 -1
  38. package/dist/engine/boot-validator/access-declarations.js +12 -3
  39. package/dist/engine/boot-validator/entity-handler.js +13 -1
  40. package/dist/engine/boot-validator/entity-list-screens.d.ts +1 -0
  41. package/dist/engine/boot-validator/entity-list-screens.js +2 -2
  42. package/dist/engine/boot-validator/index.js +6 -6
  43. package/dist/engine/boot-validator/nav.d.ts +2 -1
  44. package/dist/engine/boot-validator/nav.js +10 -20
  45. package/dist/engine/boot-validator/ownership.d.ts +1 -1
  46. package/dist/engine/boot-validator/ownership.js +2 -1
  47. package/dist/engine/boot-validator/pii-retention.d.ts +2 -0
  48. package/dist/engine/boot-validator/pii-retention.js +5 -2
  49. package/dist/engine/boot-validator/projection-list-screens.js +8 -0
  50. package/dist/engine/boot-validator/query-output-columns.js +53 -5
  51. package/dist/engine/boot-validator/screens.d.ts +2 -2
  52. package/dist/engine/boot-validator/screens.js +125 -46
  53. package/dist/engine/boot-validator/transfer-graph.js +9 -6
  54. package/dist/engine/boot-validator/url-prefill-fields.js +8 -6
  55. package/dist/engine/boot-validator/zod-shape.js +17 -4
  56. package/dist/engine/build-app-schema.js +65 -20
  57. package/dist/engine/build-config-feature-schema.js +8 -1
  58. package/dist/engine/changeset-changes.js +24 -10
  59. package/dist/engine/create-app.js +10 -1
  60. package/dist/engine/extension-names.d.ts +4 -0
  61. package/dist/engine/extension-names.js +4 -0
  62. package/dist/engine/feature-ast/extractors/events.js +11 -8
  63. package/dist/engine/feature-ast/extractors/handlers.js +39 -16
  64. package/dist/engine/feature-ast/extractors/resolve-import.d.ts +7 -2
  65. package/dist/engine/feature-ast/extractors/resolve-import.js +42 -15
  66. package/dist/engine/feature-ast/parse.d.ts +1 -0
  67. package/dist/engine/feature-ast/parse.js +18 -3
  68. package/dist/engine/feature-ast/patch.js +80 -61
  69. package/dist/engine/feature-ast/patcher.d.ts +1 -0
  70. package/dist/engine/feature-ast/patcher.js +2 -1
  71. package/dist/engine/feature-ast/render.js +1 -1
  72. package/dist/engine/feature-entity-handlers.d.ts +1 -0
  73. package/dist/engine/feature-entity-handlers.js +2 -0
  74. package/dist/engine/index.d.ts +4 -3
  75. package/dist/engine/index.js +3 -2
  76. package/dist/engine/ownership.d.ts +1 -0
  77. package/dist/engine/ownership.js +15 -2
  78. package/dist/engine/pattern-library/mixed-schemas.js +15 -29
  79. package/dist/engine/pattern-library/shared-fields.d.ts +2 -0
  80. package/dist/engine/pattern-library/shared-fields.js +17 -0
  81. package/dist/engine/registry-facade.js +3 -0
  82. package/dist/engine/registry-validate.d.ts +1 -0
  83. package/dist/engine/registry-validate.js +14 -0
  84. package/dist/engine/registry.js +2 -1
  85. package/dist/engine/role-assignment.d.ts +7 -1
  86. package/dist/engine/role-assignment.js +27 -7
  87. package/dist/engine/schema-builder.d.ts +1 -0
  88. package/dist/engine/schema-builder.js +9 -2
  89. package/dist/engine/screen-helpers.js +15 -4
  90. package/dist/engine/steps/_event-match.js +34 -7
  91. package/dist/engine/steps/_read-source.js +8 -0
  92. package/dist/engine/system-user.d.ts +1 -0
  93. package/dist/engine/system-user.js +3 -0
  94. package/dist/engine/where-rule-lint.js +22 -4
  95. package/dist/env/dry-run.js +10 -3
  96. package/dist/env/index.js +21 -1
  97. package/dist/errors/reasons.d.ts +0 -1
  98. package/dist/errors/reasons.js +2 -5
  99. package/dist/event-store/event-store.js +10 -2
  100. package/dist/event-store/provenance-append.js +4 -1
  101. package/dist/files/file-routes.d.ts +0 -1
  102. package/dist/files/file-routes.js +22 -7
  103. package/dist/files/local-provider.js +13 -10
  104. package/dist/files/provider-resolver.js +20 -1
  105. package/dist/files/types.js +16 -1
  106. package/dist/i18n/request-locale.d.ts +3 -2
  107. package/dist/i18n/request-locale.js +16 -6
  108. package/dist/i18n/required-surface-keys.js +23 -35
  109. package/dist/i18n/settings-hub-keys.js +3 -0
  110. package/dist/jobs/index.d.ts +2 -2
  111. package/dist/jobs/index.js +1 -1
  112. package/dist/jobs/job-runner.d.ts +2 -0
  113. package/dist/jobs/job-runner.js +172 -59
  114. package/dist/migrations/index.d.ts +1 -1
  115. package/dist/migrations/index.js +1 -1
  116. package/dist/migrations/pending-rebuilds.d.ts +4 -0
  117. package/dist/migrations/pending-rebuilds.js +23 -1
  118. package/dist/observability/escape-hatch-report.d.ts +1 -0
  119. package/dist/observability/escape-hatch-report.js +3 -0
  120. package/dist/observability/index.d.ts +2 -1
  121. package/dist/observability/index.js +2 -1
  122. package/dist/observability/standard-metrics.d.ts +4 -0
  123. package/dist/observability/standard-metrics.js +16 -0
  124. package/dist/pipeline/dispatch-query.d.ts +1 -1
  125. package/dist/pipeline/dispatch-query.js +2 -2
  126. package/dist/pipeline/dispatch-shared.d.ts +2 -3
  127. package/dist/pipeline/dispatch-shared.js +14 -9
  128. package/dist/pipeline/dispatch-stream.js +2 -5
  129. package/dist/pipeline/dispatch-write.js +22 -5
  130. package/dist/pipeline/dispatcher.js +1 -2
  131. package/dist/pipeline/event-dispatcher-admin.js +12 -2
  132. package/dist/pipeline/event-dispatcher-delivery.js +5 -0
  133. package/dist/pipeline/event-dispatcher.d.ts +1 -0
  134. package/dist/pipeline/event-dispatcher.js +30 -12
  135. package/dist/pipeline/index.d.ts +1 -1
  136. package/dist/pipeline/index.js +1 -1
  137. package/dist/pipeline/member-read-only-transaction.js +1 -14
  138. package/dist/pipeline/msp-rebuild.d.ts +1 -0
  139. package/dist/pipeline/msp-rebuild.js +1 -0
  140. package/dist/pipeline/system-hooks.d.ts +1 -1
  141. package/dist/pipeline/system-hooks.js +19 -20
  142. package/dist/pipeline/tenant-timezone-cache.d.ts +2 -0
  143. package/dist/pipeline/tenant-timezone-cache.js +20 -6
  144. package/dist/pipeline/write-origin.js +13 -6
  145. package/dist/redis/pubsub-signal.d.ts +2 -0
  146. package/dist/redis/pubsub-signal.js +2 -2
  147. package/dist/schema-cli.js +4 -14
  148. package/dist/search/meilisearch-adapter.js +17 -6
  149. package/dist/search/purge-subject.js +8 -3
  150. package/dist/stack/drain-event-consumers.js +3 -0
  151. package/dist/stack/drain-jobs.d.ts +3 -1
  152. package/dist/stack/drain-jobs.js +55 -13
  153. package/dist/stack/table-helpers.js +10 -8
  154. package/dist/testing/closed-connection-error.js +2 -3
  155. package/dist/testing/wait-for.js +1 -0
  156. package/dist/ui-types/index.d.ts +1 -1
  157. package/dist/ui-types/index.js +1 -1
  158. package/dist/upgrade-cli.d.ts +1 -0
  159. package/dist/upgrade-cli.js +36 -15
  160. package/package.json +4 -4
  161. package/src/changes.json +264 -0
  162. package/src/scripts/codemod/migrate-cross-tenant.ts +14 -2
  163. package/src/scripts/codemod/migrate-db-raw.ts +16 -8
  164. package/src/scripts/codemod/migrate-open-to-all.ts +7 -1
  165. package/src/scripts/codemod/pii-personal-migration.ts +111 -44
  166. package/src/scripts/codemod/unprocessable-error-details-reason.ts +136 -42
  167. package/dist/scripts/codemod/crypto-shredding-testing-move.d.ts +0 -2
  168. package/dist/scripts/codemod/crypto-shredding-testing-move.js +0 -109
  169. package/dist/scripts/codemod/migrate-cross-tenant.d.ts +0 -19
  170. package/dist/scripts/codemod/migrate-cross-tenant.js +0 -266
  171. package/dist/scripts/codemod/migrate-db-raw.d.ts +0 -23
  172. package/dist/scripts/codemod/migrate-db-raw.js +0 -345
  173. package/dist/scripts/codemod/migrate-open-to-all.d.ts +0 -22
  174. package/dist/scripts/codemod/migrate-open-to-all.js +0 -194
  175. package/dist/scripts/codemod/pii-personal-migration.d.ts +0 -11
  176. package/dist/scripts/codemod/pii-personal-migration.js +0 -615
  177. package/dist/scripts/codemod/unprocessable-error-details-reason.d.ts +0 -2
  178. package/dist/scripts/codemod/unprocessable-error-details-reason.js +0 -235
@@ -162,7 +162,7 @@ export function createFeaturePatcher(sourceFile) {
162
162
  ...(escapeHatch !== undefined && { escapeHatch }),
163
163
  });
164
164
  },
165
- addStreamHandler({ name, schemaSource, handlerSource, access, rateLimit }) {
165
+ addStreamHandler({ name, schemaSource, handlerSource, access, rateLimit, escapeHatch }) {
166
166
  add({
167
167
  kind: "streamHandler",
168
168
  source: SYNTHETIC_LOC,
@@ -171,6 +171,7 @@ export function createFeaturePatcher(sourceFile) {
171
171
  handlerBody: rawLoc(handlerSource),
172
172
  ...(access !== undefined && { access }),
173
173
  ...(rateLimit !== undefined && { rateLimit }),
174
+ ...(escapeHatch !== undefined && { escapeHatch }),
174
175
  });
175
176
  },
176
177
  addHook({ type, target, handlerSource, phase, escapeHatch }) {
@@ -640,7 +640,7 @@ function renderValueWithRawSlots(value, indent) {
640
640
  }
641
641
  const DEFAULT_IMPORTS = [
642
642
  'import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";',
643
- 'import { z } from "zod";',
643
+ 'import * as z from "zod";',
644
644
  ];
645
645
  /**
646
646
  * Render a complete feature-file: schema-version header, imports, the
@@ -25,6 +25,7 @@ export declare function buildEntityHandlerMethods<TName extends string>(state: F
25
25
  access: AccessRule;
26
26
  rateLimit?: RateLimitDeclaration;
27
27
  outputSchema?: ZodType;
28
+ liveEntities?: readonly string[];
28
29
  description?: string;
29
30
  agent?: AgentHandlerHints;
30
31
  escapeHatch?: EscapeHatchDeclaration;
@@ -146,6 +146,7 @@ export function buildEntityHandlerMethods(state, name) {
146
146
  ...agentSlots(def),
147
147
  ...(def.rateLimit && { rateLimit: def.rateLimit }),
148
148
  ...(def.outputSchema && { outputSchema: def.outputSchema }),
149
+ ...(def.liveEntities && { liveEntities: def.liveEntities }),
149
150
  ...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
150
151
  // Carry the definePagedQueryHandler brand through — this rebuild
151
152
  // drops any field not explicitly listed.
@@ -167,6 +168,7 @@ export function buildEntityHandlerMethods(state, name) {
167
168
  ...agentSlots(inline.options),
168
169
  ...(inline.options.rateLimit && { rateLimit: inline.options.rateLimit }),
169
170
  ...(inline.options.outputSchema && { outputSchema: inline.options.outputSchema }),
171
+ ...(inline.options.liveEntities && { liveEntities: inline.options.liveEntities }),
170
172
  ...(inline.options.escapeHatch && { escapeHatch: inline.options.escapeHatch }),
171
173
  };
172
174
  tryMapEntity(state, name, nameOrDef);
@@ -1,6 +1,7 @@
1
1
  export { hasAccess } from "./access.js";
2
2
  export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, type PrincipalProfile, type PrincipalStatus, type PrincipalStatusPlugin, TENANT_TEARDOWN_STATUSES, type TenantLifecycleStatusPlugin, } from "./active-membership.js";
3
3
  export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
4
+ export { entityHasAnonymizableSubjectField } from "./boot-validator/pii-retention.js";
4
5
  export { collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, type ValidateBootOptions, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
5
6
  export { type BuildAppSchemaOptions, buildAppSchema } from "./build-app-schema.js";
6
7
  export type { ConfigFeatureSchema } from "./build-config-feature-schema.js";
@@ -58,11 +59,11 @@ export { createRegistry } from "./registry.js";
58
59
  export type { ClampInfo, ResolveOptions } from "./resolve-config-or-param.js";
59
60
  export { resolveConfigOrParam } from "./resolve-config-or-param.js";
60
61
  export type { AssignableAppRoles, AssignableFromRole } from "./role-assignment.js";
61
- export { assignableAppRolesFromUsages, findForbiddenRoleAssignment, isAssignableByRole, } from "./role-assignment.js";
62
+ export { assignableAppRolesFromUsages, assignableAppRolesOf, canActorAssignRole, findForbiddenRoleAssignment, isAssignableByRole, mergeAssignedRoles, } from "./role-assignment.js";
62
63
  export { runsInLane } from "./run-in.js";
63
64
  export type { StepListOutcome } from "./run-pipeline.js";
64
65
  export { runPipeline, runStepList } from "./run-pipeline.js";
65
- export { buildInsertSchema, buildUpdateSchema, fieldToZod } from "./schema-builder.js";
66
+ export { buildInsertSchema, buildUpdateSchema, fieldToZod, SYSTEM_ONLY_JSON_SCHEMA_KEY, } from "./schema-builder.js";
66
67
  export { isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, normalizeListColumn, sectionFieldSpecs, } from "./screen-helpers.js";
67
68
  export type { TransitionGraph } from "./state-machine.js";
68
69
  export { defineTransitions, guardTransition } from "./state-machine.js";
@@ -72,7 +73,7 @@ export { describeWorkflowStepError } from "./steps/describe-workflow-step-error.
72
73
  export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
73
74
  export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
74
75
  export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
75
- 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, 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, 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
77
  export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, HookPhases, isAgentVisibleScreen, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
77
78
  export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.js";
78
79
  export { runValidation } from "./validation.js";
@@ -2,6 +2,7 @@
2
2
  export { hasAccess } from "./access.js";
3
3
  export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, TENANT_TEARDOWN_STATUSES, } from "./active-membership.js";
4
4
  export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
5
+ export { entityHasAnonymizableSubjectField } from "./boot-validator/pii-retention.js";
5
6
  export { collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
6
7
  export { buildAppSchema } from "./build-app-schema.js";
7
8
  export { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, SETTINGS_HUB_WORKSPACE, } from "./build-config-feature-schema.js";
@@ -54,10 +55,10 @@ export { isValidQn, parseQn, QnTypes, qn, toKebab } from "./qualified-name.js";
54
55
  export { readClaim } from "./read-claim.js";
55
56
  export { createRegistry } from "./registry.js";
56
57
  export { resolveConfigOrParam } from "./resolve-config-or-param.js";
57
- export { assignableAppRolesFromUsages, findForbiddenRoleAssignment, isAssignableByRole, } from "./role-assignment.js";
58
+ export { assignableAppRolesFromUsages, assignableAppRolesOf, canActorAssignRole, findForbiddenRoleAssignment, isAssignableByRole, mergeAssignedRoles, } from "./role-assignment.js";
58
59
  export { runsInLane } from "./run-in.js";
59
60
  export { runPipeline, runStepList } from "./run-pipeline.js";
60
- export { buildInsertSchema, buildUpdateSchema, fieldToZod } from "./schema-builder.js";
61
+ export { buildInsertSchema, buildUpdateSchema, fieldToZod, SYSTEM_ONLY_JSON_SCHEMA_KEY, } from "./schema-builder.js";
61
62
  export { isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, normalizeListColumn, sectionFieldSpecs, } from "./screen-helpers.js";
62
63
  export { defineTransitions, guardTransition } from "./state-machine.js";
63
64
  export { evaluateEventMatch } from "./steps/_event-match.js";
@@ -7,6 +7,7 @@ export declare function userCanReadFieldRow(user: SessionUser, accessMap: Owners
7
7
  export declare function userCanWriteFieldRow(user: SessionUser, accessMap: OwnershipMap | undefined, oldRow: Readonly<Record<string, unknown>>, newRow: Readonly<Record<string, unknown>>): boolean;
8
8
  export declare function normalizeAccessEntry(entry: OwnershipMap | readonly string[] | undefined): OwnershipMap | undefined;
9
9
  export declare function userCanCreateFieldRow(user: SessionUser, accessMap: OwnershipMap | undefined, newRow: Readonly<Record<string, unknown>>): boolean;
10
+ export declare function tableNameOf(table: unknown): string;
10
11
  export declare function shiftParams(fragment: SqlFragment, shift: number): SqlFragment;
11
12
  export declare function buildOwnershipClause(user: SessionUser, accessMap: OwnershipMap | undefined, table: unknown, paramStart?: number): OwnershipClause;
12
13
  export declare function combineClauses(a: OwnershipClause, b: OwnershipClause): OwnershipClause;
@@ -185,7 +185,7 @@ export function userCanCreateFieldRow(user, accessMap, newRow) {
185
185
  }
186
186
  const PASS_CLAUSE = { kind: "pass" };
187
187
  const EMPTY_CLAUSE = { kind: "empty" };
188
- function tableNameOf(table) {
188
+ export function tableNameOf(table) {
189
189
  if (table !== null && typeof table === "object") {
190
190
  const sym = table[KUMIKO_NAME_SYMBOL];
191
191
  if (typeof sym === "string")
@@ -298,6 +298,19 @@ export function combineClauses(a, b) {
298
298
  params: [...a.params, ...shifted.params],
299
299
  };
300
300
  }
301
+ // The column set is constant per table object, but ruleToFragment runs on every
302
+ // read with a where-rule.
303
+ const columnSqlNamesByTable = new WeakMap();
304
+ function cachedTableColumnSqlNames(table) {
305
+ if (table === null || typeof table !== "object")
306
+ return tableColumnSqlNames(table);
307
+ let names = columnSqlNamesByTable.get(table);
308
+ if (!names) {
309
+ names = tableColumnSqlNames(table);
310
+ columnSqlNamesByTable.set(table, names);
311
+ }
312
+ return names;
313
+ }
301
314
  function ruleToFragment(rule, user, table, paramStart) {
302
315
  if (rule === "all") {
303
316
  return { kind: "sql", sqlText: "TRUE", params: [] };
@@ -311,7 +324,7 @@ function ruleToFragment(rule, user, table, paramStart) {
311
324
  // Runtime backstop for the boot-time probe (boot-validator/ownership.ts):
312
325
  // an unqualified column in a correlated subquery silently turns into a
313
326
  // fail-open tautology (fw#2639) — throw instead of shipping it.
314
- assertQualifiedWhereFragment(frag.sqlText, tableColumnSqlNames(table), `ownership where-rule on "${tableNameOf(table)}"`);
327
+ assertQualifiedWhereFragment(frag.sqlText, cachedTableColumnSqlNames(table), `ownership where-rule on "${tableNameOf(table)}"`);
315
328
  return { kind: "sql", sqlText: frag.sqlText, params: frag.params };
316
329
  }
317
330
  // FromRule
@@ -1,5 +1,5 @@
1
1
  // Mixed pattern schemas (header form + opaque body source).
2
- import { accessRuleField, HOOK_TYPE_OPTIONS, HTTP_METHOD_OPTIONS } from "./shared-fields.js";
2
+ import { accessRuleField, agentDescriptionField, agentHintsField, HOOK_TYPE_OPTIONS, HTTP_METHOD_OPTIONS, } from "./shared-fields.js";
3
3
  // --- Mixed patterns (header form + opaque body source) --------------------
4
4
  export const screenSchema = {
5
5
  kind: "screen",
@@ -71,20 +71,8 @@ export const writeHandlerSchema = {
71
71
  readOnly: true,
72
72
  },
73
73
  accessRuleField,
74
- {
75
- path: "description",
76
- label: { en: "Description", de: "Beschreibung" },
77
- hint: {
78
- en: "Prose the AI agent sees as the tool description. Without it the handler stays invisible to the agent.",
79
- },
80
- input: "textarea",
81
- },
82
- {
83
- path: "agent",
84
- label: { en: "Agent hints", de: "Agent-Hinweise" },
85
- hint: { en: "expose override + risk level for the AI-agent manifest." },
86
- input: "json-readonly",
87
- },
74
+ agentDescriptionField,
75
+ agentHintsField,
88
76
  {
89
77
  path: "rateLimit",
90
78
  label: { en: "Rate limit", de: "Rate-Limit" },
@@ -126,20 +114,8 @@ export const queryHandlerSchema = {
126
114
  readOnly: true,
127
115
  },
128
116
  accessRuleField,
129
- {
130
- path: "description",
131
- label: { en: "Description", de: "Beschreibung" },
132
- hint: {
133
- en: "Prose the AI agent sees as the tool description. Without it the handler stays invisible to the agent.",
134
- },
135
- input: "textarea",
136
- },
137
- {
138
- path: "agent",
139
- label: { en: "Agent hints", de: "Agent-Hinweise" },
140
- hint: { en: "expose override + risk level for the AI-agent manifest." },
141
- input: "json-readonly",
142
- },
117
+ agentDescriptionField,
118
+ agentHintsField,
143
119
  {
144
120
  path: "rateLimit",
145
121
  label: { en: "Rate limit", de: "Rate-Limit" },
@@ -549,6 +525,16 @@ export const defineEventSchema = {
549
525
  language: "zod",
550
526
  readOnly: true,
551
527
  },
528
+ {
529
+ path: "piiFields",
530
+ label: { en: "PII fields (source)", de: "PII-Felder (Source)" },
531
+ hint: {
532
+ en: 'Which payload fields are encrypted, or "none" when the event carries no PII.',
533
+ },
534
+ input: "code-block",
535
+ language: "typescript",
536
+ readOnly: true,
537
+ },
552
538
  {
553
539
  path: "version",
554
540
  label: { en: "Version", de: "Version" },
@@ -117,4 +117,6 @@ export declare const ID_TYPE_OPTIONS: readonly [{
117
117
  readonly de: "Serial Integer";
118
118
  };
119
119
  }];
120
+ export declare const agentDescriptionField: FormFieldSpec;
121
+ export declare const agentHintsField: FormFieldSpec;
120
122
  export declare const accessRuleField: FormFieldSpec;
@@ -30,6 +30,23 @@ export const ID_TYPE_OPTIONS = [
30
30
  { value: "uuid", label: { en: "UUID (default)", de: "UUID (Standard)" } },
31
31
  { value: "serial", label: { en: "Serial integer", de: "Serial Integer" } },
32
32
  ];
33
+ export const agentDescriptionField = {
34
+ path: "description",
35
+ label: { en: "Description", de: "Beschreibung" },
36
+ hint: {
37
+ en: "Prose the AI agent sees as the tool description. Without it the handler stays invisible to the agent.",
38
+ },
39
+ input: "textarea",
40
+ };
41
+ export const agentHintsField = {
42
+ path: "agent",
43
+ label: { en: "Agent hints", de: "Agent-Hinweise" },
44
+ hint: {
45
+ en: "expose override + risk level for the AI-agent manifest.",
46
+ de: "Expose-Override und Risikostufe für das KI-Agent-Manifest.",
47
+ },
48
+ input: "json-readonly",
49
+ };
33
50
  export const accessRuleField = {
34
51
  path: "access",
35
52
  label: { en: "Access", de: "Zugriff" },
@@ -113,6 +113,9 @@ export function buildRegistryFacade(state) {
113
113
  getHandlerFeature(qualifiedHandler) {
114
114
  return state.handlerFeatureMap.get(qualifiedHandler);
115
115
  },
116
+ getJobFeature(qualifiedJobName) {
117
+ return state.jobFeatureMap.get(qualifiedJobName);
118
+ },
116
119
  getAllMetrics() {
117
120
  return state.metricMap;
118
121
  },
@@ -17,6 +17,7 @@ export declare function resolveNotificationTriggersAndRegisterHooks(state: Regis
17
17
  export declare function validateLifecycleHookTargets(state: RegistryState): void;
18
18
  export declare function validateEntityHookTargets(state: RegistryState, features: readonly FeatureDefinition[]): void;
19
19
  export declare function validateJobTriggers(state: RegistryState): void;
20
+ export declare function validateLiveEntities(state: RegistryState): void;
20
21
  export declare function validateBootGates(state: RegistryState): void;
21
22
  export declare function validateJobBackoff(state: RegistryState): void;
22
23
  export declare function validateExtensionUsageTargets(state: RegistryState): void;
@@ -568,6 +568,15 @@ export function validateJobTriggers(state) {
568
568
  }
569
569
  }
570
570
  }
571
+ export function validateLiveEntities(state) {
572
+ for (const [handlerQn, handler] of state.queryHandlerMap) {
573
+ for (const entityName of handler.liveEntities ?? []) {
574
+ if (state.entityMap.has(entityName))
575
+ continue;
576
+ throw new Error(`Query handler "${handlerQn}" declares liveEntities "${entityName}" but no entity with that name is registered`);
577
+ }
578
+ }
579
+ }
571
580
  export function validateBootGates(state) {
572
581
  // Boot gates run inline in JobRunner.start(), which has neither the
573
582
  // per-tenant fan-out (that re-enqueues) nor the sequential lock's
@@ -582,6 +591,11 @@ export function validateBootGates(state) {
582
591
  if (jobDef.concurrency === "sequential") {
583
592
  throw new Error(`Job "${jobName}" cannot combine bootGate with concurrency "sequential"`);
584
593
  }
594
+ // The gate already runs inline each start; runOnBoot would enqueue the same
595
+ // handler again under the same boot job id (runs twice, shared bull_job_id).
596
+ if (jobDef.runOnBoot) {
597
+ throw new Error(`Job "${jobName}" cannot combine bootGate with runOnBoot`);
598
+ }
585
599
  }
586
600
  }
587
601
  export function validateJobBackoff(state) {
@@ -3,7 +3,7 @@ import { dedupeFeatures } from "./dedupe-features.js";
3
3
  import { buildRegistryFacade } from "./registry-facade.js";
4
4
  import { populateClaimsAndAuth, populateConfigKeys, populateEvents, populateExtensionsAndSeeds, populateFeatureCore, populateHandlers, populateHooks, populateJobsAndNotifications, populateMetricsAndSecrets, populateProjectionsAndTables, populateScreensNavWorkspaces, populateTranslations, } from "./registry-ingest.js";
5
5
  import { createInitialState } from "./registry-state.js";
6
- import { applyExtensionUsages, autoWireSoftDeleteJobs, buildEventUpcasterChains, buildImplicitProjections, buildIncomingRelations, buildSearchableSortableCaches, computeHasRateLimitedHandler, finalizeWorkspaceNavMembership, populateHandlerEntityMappings, publishEventPiiCatalog, resolveNotificationTriggersAndRegisterHooks, validateBootGates, validateEntityHookTargets, validateEventMigrationVersions, validateExtensionSelectors, validateExtensionUsageTargets, validateFieldAccessHandlersAreEntityMapped, validateJobBackoff, validateJobTriggers, validateLifecycleHookTargets, validateProjectionApplyKeys, validateRelationTargetsExist, validateRequiredFeatures, } from "./registry-validate.js";
6
+ import { applyExtensionUsages, autoWireSoftDeleteJobs, buildEventUpcasterChains, buildImplicitProjections, buildIncomingRelations, buildSearchableSortableCaches, computeHasRateLimitedHandler, finalizeWorkspaceNavMembership, populateHandlerEntityMappings, publishEventPiiCatalog, resolveNotificationTriggersAndRegisterHooks, validateBootGates, validateEntityHookTargets, validateEventMigrationVersions, validateExtensionSelectors, validateExtensionUsageTargets, validateFieldAccessHandlersAreEntityMapped, validateJobBackoff, validateJobTriggers, validateLifecycleHookTargets, validateLiveEntities, validateProjectionApplyKeys, validateRelationTargetsExist, validateRequiredFeatures, } from "./registry-validate.js";
7
7
  // This is where the magic happens. By "magic" I mean: precomputed maps.
8
8
  // I build everything once at boot (hooks, relations, searchable fields, ...)
9
9
  // so nothing has to iterate over objects at runtime. O(1) instead of O(n*m).
@@ -43,6 +43,7 @@ export function createRegistry(rawFeatures) {
43
43
  validateEntityHookTargets(state, features);
44
44
  validateJobTriggers(state);
45
45
  validateBootGates(state);
46
+ validateLiveEntities(state);
46
47
  validateJobBackoff(state);
47
48
  validateExtensionUsageTargets(state);
48
49
  computeHasRateLimitedHandler(state);
@@ -1,4 +1,6 @@
1
- export type AssignableFromRole = "User" | "Member" | "Editor" | "Admin" | "TenantAdmin" | "SystemAdmin";
1
+ import type { Registry } from "./types/feature.js";
2
+ declare const ASSIGNABLE_FROM_ROLE_LIST: readonly ["User", "Member", "Editor", "Admin", "TenantAdmin", "SystemAdmin"];
3
+ export type AssignableFromRole = (typeof ASSIGNABLE_FROM_ROLE_LIST)[number];
2
4
  /** App role → lowest built-in role allowed to assign it. */
3
5
  export type AssignableAppRoles = ReadonlyMap<string, AssignableFromRole>;
4
6
  export declare const DEFAULT_APP_ROLE_ASSIGNABLE_FROM: AssignableFromRole;
@@ -8,4 +10,8 @@ export declare function assignableAppRolesFromUsages(usages: readonly {
8
10
  }[]): AssignableAppRoles;
9
11
  /** True when `actorRole` ranks at least as high as the role an app role is assignable from. */
10
12
  export declare function isAssignableByRole(assignableFrom: AssignableFromRole, actorRole: AssignableFromRole): boolean;
13
+ export declare function canActorAssignRole(actorRoles: readonly string[], role: string, assignableAppRoles?: AssignableAppRoles): boolean;
14
+ export declare function mergeAssignedRoles(actorRoles: readonly string[], assignedRoles: readonly string[], targetCurrentRoles: readonly string[], assignableAppRoles: AssignableAppRoles): readonly string[];
15
+ export declare function assignableAppRolesOf(registry: Pick<Registry, "getExtensionUsages">): AssignableAppRoles;
11
16
  export declare function findForbiddenRoleAssignment(actorRoles: readonly string[], assignedRoles: readonly string[], targetCurrentRoles: readonly string[], assignableAppRoles?: AssignableAppRoles): string | undefined;
17
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { ROLES } from "../auth/roles.js";
2
+ import { EXT_ASSIGNABLE_ROLE } from "./extension-names.js";
2
3
  import { isForbiddenMembershipRole } from "./membership-roles.js";
3
4
  const PLATFORM_ROLE_NAMES = new Set(Object.values(ROLES));
4
5
  // Prototype-free rank table — Object literals leak Object.prototype
@@ -15,18 +16,19 @@ const ROLE_RANKS = new Map([
15
16
  ["SystemAdmin", 4],
16
17
  ["system", 5],
17
18
  ]);
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([
19
+ const ASSIGNABLE_FROM_ROLE_LIST = [
23
20
  "User",
24
21
  "Member",
25
22
  "Editor",
26
23
  "Admin",
27
24
  "TenantAdmin",
28
25
  "SystemAdmin",
29
- ]);
26
+ ];
27
+ // App roles are ordinary, non-privileged tiers: whoever may manage members
28
+ // (access.admin) may grant them; an app raises the bar via assignableFrom.
29
+ export const DEFAULT_APP_ROLE_ASSIGNABLE_FROM = "Admin";
30
+ const NO_ASSIGNABLE_APP_ROLES = new Map();
31
+ const ASSIGNABLE_FROM_ROLES = new Set(ASSIGNABLE_FROM_ROLE_LIST);
30
32
  function isAssignableFromRole(value) {
31
33
  return typeof value === "string" && ASSIGNABLE_FROM_ROLES.has(value);
32
34
  }
@@ -88,6 +90,24 @@ function requiredAssignRank(role, assignableAppRoles) {
88
90
  const assignableFrom = assignableAppRoles.get(role);
89
91
  return assignableFrom === undefined ? Number.POSITIVE_INFINITY : getRoleRank(assignableFrom);
90
92
  }
93
+ export function canActorAssignRole(actorRoles, role, assignableAppRoles = NO_ASSIGNABLE_APP_ROLES) {
94
+ return requiredAssignRank(role, assignableAppRoles) <= maxRoleRank(actorRoles);
95
+ }
96
+ // An actor only removes what it could also grant; clients (the member form)
97
+ // cannot render roles outside their option list, so those must survive a save.
98
+ export function mergeAssignedRoles(actorRoles, assignedRoles, targetCurrentRoles, assignableAppRoles) {
99
+ const preserved = targetCurrentRoles.filter((role) => !assignedRoles.includes(role) && !canActorAssignRole(actorRoles, role, assignableAppRoles));
100
+ return [...new Set([...assignedRoles, ...preserved])];
101
+ }
102
+ const assignableAppRolesByRegistry = new WeakMap();
103
+ export function assignableAppRolesOf(registry) {
104
+ const cached = assignableAppRolesByRegistry.get(registry);
105
+ if (cached !== undefined)
106
+ return cached;
107
+ const built = assignableAppRolesFromUsages(registry.getExtensionUsages(EXT_ASSIGNABLE_ROLE));
108
+ assignableAppRolesByRegistry.set(registry, built);
109
+ return built;
110
+ }
91
111
  export function findForbiddenRoleAssignment(actorRoles, assignedRoles,
92
112
  // Required so callers cannot accidentally disable the downgrade/
93
113
  // takeover guard by omitting the third argument (new users: pass []).
@@ -96,7 +116,7 @@ targetCurrentRoles, assignableAppRoles = NO_ASSIGNABLE_APP_ROLES) {
96
116
  // Assign path: fail-closed on unknown / above-actor roles — except unranked
97
117
  // app roles the target already holds (round-trip restore after strip).
98
118
  const forbiddenAssigned = assignedRoles.find((role) => {
99
- if (requiredAssignRank(role, assignableAppRoles) <= actorRank)
119
+ if (canActorAssignRole(actorRoles, role, assignableAppRoles))
100
120
  return false;
101
121
  if (knownRoleRank(role) === undefined && targetCurrentRoles.includes(role))
102
122
  return false;
@@ -5,6 +5,7 @@ export declare function fieldToZod(field: FieldDefinition, currencies: readonly
5
5
  readonly applyDefaults?: boolean;
6
6
  readonly isUpdate?: boolean;
7
7
  }): z.ZodTypeAny;
8
+ export declare const SYSTEM_ONLY_JSON_SCHEMA_KEY = "x-kumiko-system-only";
8
9
  export declare function buildInsertSchema(entity: EntityDefinition, currencies?: readonly string[], excludedFields?: readonly string[], options?: {
9
10
  readonly allowCallerId?: boolean;
10
11
  }): z.ZodObject<Record<string, z.ZodTypeAny>>;
@@ -373,6 +373,7 @@ function applyTotalsMatchRefinements(entity, schema) {
373
373
  function notWritableField() {
374
374
  return z.never().optional();
375
375
  }
376
+ export const SYSTEM_ONLY_JSON_SCHEMA_KEY = "x-kumiko-system-only";
376
377
  export function buildInsertSchema(entity, currencies = [...DEFAULT_CURRENCIES], excludedFields = [], options = {}) {
377
378
  const shape = {};
378
379
  for (const [name, field] of Object.entries(entity.fields)) {
@@ -387,8 +388,14 @@ export function buildInsertSchema(entity, currencies = [...DEFAULT_CURRENCIES],
387
388
  }
388
389
  // Opt-in only for the generic create verb, which honors the id for system identities;
389
390
  // other schema consumers must keep stripping an unknown `id`.
390
- if (options.allowCallerId)
391
- shape["id"] = z.uuid().optional();
391
+ if (options.allowCallerId) {
392
+ // Marked so agent-facing exports can drop it: agent dispatch never runs as
393
+ // a system identity, so a caller-chosen id would be silently discarded.
394
+ shape["id"] = z
395
+ .uuid()
396
+ .optional()
397
+ .meta({ [SYSTEM_ONLY_JSON_SCHEMA_KEY]: true });
398
+ }
392
399
  return applyTotalsMatchRefinements(entity, z.object(shape));
393
400
  }
394
401
  export function buildUpdateSchema(entity, currencies = [...DEFAULT_CURRENCIES], excludedFields = []) {
@@ -12,21 +12,32 @@ export function explicitListScreenId(screen) {
12
12
  return undefined;
13
13
  }
14
14
  }
15
+ // Screen refs may be short ids or `<feature>:screen:<id>` QNs (boot-validator
16
+ // accepts both for navigate targets and listScreenId); compare on the short id.
17
+ function screenShortId(ref) {
18
+ return ref.slice(ref.lastIndexOf(":") + 1);
19
+ }
15
20
  // Shared by the renderer breadcrumb and the boot-validator so both resolve
16
21
  // parents the same way; `getId` lets each caller normalize `screen.id`.
17
22
  export function resolveNavParentScreen(screens, detail, getId) {
18
- const detailScreenId = getId(detail);
23
+ const detailScreenId = screenShortId(getId(detail));
19
24
  const listFromExplicit = (() => {
20
25
  const explicitId = explicitListScreenId(detail);
21
- return explicitId !== undefined ? screens.find((s) => getId(s) === explicitId) : undefined;
26
+ return explicitId !== undefined
27
+ ? screens.find((s) => screenShortId(getId(s)) === screenShortId(explicitId))
28
+ : undefined;
22
29
  })();
23
30
  // rowActions, toolbarActions, and drawer-kind actions (which mount inline
24
31
  // and have no page of their own) all count as "reached from" this list.
25
- const navigatesToDetail = (actions) => (actions ?? []).some((a) => (a.kind === "navigate" || a.kind === "drawer") && a.screen === detailScreenId);
32
+ const navigatesToDetail = (actions) => (actions ?? []).some((a) => (a.kind === "navigate" || a.kind === "drawer") &&
33
+ a.screen !== undefined &&
34
+ screenShortId(a.screen) === detailScreenId);
26
35
  // The create button of an entityList navigates to its createScreen, so the
27
36
  // list is that screen's parent just like for a rowAction target.
28
37
  const listFromRowAction = screens.find((s) => {
29
- if (s.type === "entityList" && s.createScreen === detailScreenId)
38
+ if (s.type === "entityList" &&
39
+ s.createScreen !== undefined &&
40
+ screenShortId(s.createScreen) === detailScreenId)
30
41
  return true;
31
42
  if (s.type !== "entityList" && s.type !== "projectionList")
32
43
  return false;
@@ -5,14 +5,16 @@
5
5
  function isRecord(value) {
6
6
  return typeof value === "object" && value !== null;
7
7
  }
8
+ // A missing path must stay distinguishable from a present-but-undefined value:
9
+ // `ne` would otherwise match events that lack the field entirely.
8
10
  function resolvePath(payload, path) {
9
11
  let current = payload;
10
12
  for (const segment of path) {
11
- if (!isRecord(current))
12
- return undefined;
13
+ if (!isRecord(current) || !Object.hasOwn(current, segment))
14
+ return { found: false };
13
15
  current = current[segment];
14
16
  }
15
- return current;
17
+ return { found: true, value: current };
16
18
  }
17
19
  function compareOrdered(actual, expected, kind) {
18
20
  if (typeof actual === "number" && typeof expected === "number") {
@@ -48,6 +50,8 @@ function evaluateOp(op, actual) {
48
50
  case "ne":
49
51
  return actual !== op.value;
50
52
  case "in":
53
+ if (!Array.isArray(op.values))
54
+ throw malformed(`"in" op has no "values" array`);
51
55
  return op.values.some((value) => value === actual);
52
56
  case "gt":
53
57
  case "gte":
@@ -66,14 +70,35 @@ function evaluateOp(op, actual) {
66
70
  }
67
71
  }
68
72
  }
73
+ // The AST round-trips through jsonb, so a truncated or hand-edited row must
74
+ // fail with a readable format error rather than a bare TypeError.
75
+ function malformed(what) {
76
+ return new Error(`Malformed EventMatch: ${what}`);
77
+ }
78
+ function evaluateExprAtom(op, resolved) {
79
+ // Evaluate before checking `found` so a malformed op still throws on a
80
+ // payload that lacks the path.
81
+ const matched = evaluateOp(op, resolved.found ? resolved.value : undefined);
82
+ return resolved.found && matched;
83
+ }
69
84
  function evaluateExpr(expr, payload) {
85
+ if (!isRecord(expr))
86
+ throw malformed("expr is not an object");
70
87
  switch (expr.kind) {
71
88
  case "and":
72
- return expr.nodes.every((node) => evaluateExpr(node, payload));
73
- case "or":
74
- return expr.nodes.some((node) => evaluateExpr(node, payload));
89
+ case "or": {
90
+ if (!Array.isArray(expr.nodes))
91
+ throw malformed(`"${expr.kind}" expr has no "nodes" array`);
92
+ return expr.kind === "and"
93
+ ? expr.nodes.every((node) => evaluateExpr(node, payload))
94
+ : expr.nodes.some((node) => evaluateExpr(node, payload));
95
+ }
75
96
  case "atom":
76
- return evaluateOp(expr.op, resolvePath(payload, expr.path));
97
+ if (!Array.isArray(expr.path))
98
+ throw malformed(`"atom" expr has no "path" array`);
99
+ if (!isRecord(expr.op))
100
+ throw malformed(`"atom" expr has no "op" object`);
101
+ return evaluateExprAtom(expr.op, resolvePath(payload, expr.path));
77
102
  default: {
78
103
  const unrecognized = expr;
79
104
  throw new Error(`Unknown EventMatchExpr kind "${unrecognized.kind}"`);
@@ -81,6 +106,8 @@ function evaluateExpr(expr, payload) {
81
106
  }
82
107
  }
83
108
  export function evaluateEventMatch(match, payload) {
109
+ if (!isRecord(match))
110
+ throw malformed("match is not an object");
84
111
  if (match.version !== 1) {
85
112
  throw new Error(`Unsupported EventMatch version ${JSON.stringify(match.version)} — expected 1`);
86
113
  }
@@ -1,7 +1,15 @@
1
1
  import { unsafeRawForDeclaredStep } from "../../db/tenant-db.js";
2
+ import { InternalError } from "../../errors/classes.js";
2
3
  // unsafeRaw fails closed without the handler's escapeHatch (or systemScope) and reports the use.
3
4
  export function readSourceFor(ctx, unsafeAllTenants) {
4
5
  if (!unsafeAllTenants)
5
6
  return ctx.db;
7
+ // Step args come from untyped call sites too; a boolean `true` (the shape other
8
+ // framework APIs use) must fail as a framework error, not a TypeError on `.reason`.
9
+ if (typeof unsafeAllTenants !== "object" || typeof unsafeAllTenants.reason !== "string") {
10
+ throw new InternalError({
11
+ message: "unsafeAllTenants on a read step must be { reason: string }",
12
+ });
13
+ }
6
14
  return unsafeRawForDeclaredStep(ctx.systemDb ?? ctx.db, unsafeAllTenants.reason);
7
15
  }
@@ -5,4 +5,5 @@ export declare const SYSTEM_ROLE: "system";
5
5
  export declare function createSystemUser(tenantId: TenantId, extraRoles?: readonly string[]): SessionUser;
6
6
  export declare const ANONYMOUS_USER_ID = "anonymous";
7
7
  export declare const ANONYMOUS_ROLE: "anonymous";
8
+ export declare function isAnonymousSessionUser(user: Pick<SessionUser, "roles">): boolean;
8
9
  export declare function createAnonymousUser(tenantId: TenantId): SessionUser;
@@ -20,6 +20,9 @@ export function createSystemUser(tenantId, extraRoles = []) {
20
20
  // custom roles.
21
21
  export const ANONYMOUS_USER_ID = "anonymous";
22
22
  export const ANONYMOUS_ROLE = "anonymous";
23
+ export function isAnonymousSessionUser(user) {
24
+ return user.roles.includes(ANONYMOUS_ROLE);
25
+ }
23
26
  export function createAnonymousUser(tenantId) {
24
27
  return {
25
28
  id: ANONYMOUS_USER_ID,