@cosmicdrift/kumiko-framework 0.331.0 → 0.333.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db/blind-index-cleanup.d.ts +1 -0
- package/dist/db/blind-index-cleanup.js +12 -0
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/queries/event-consumer.d.ts +2 -1
- package/dist/db/queries/event-consumer.js +12 -2
- package/dist/engine/boot-validator/index.js +2 -0
- package/dist/engine/boot-validator/query-output-columns.js +15 -2
- package/dist/engine/boot-validator/query-refs.d.ts +2 -1
- package/dist/engine/boot-validator/query-refs.js +1 -1
- package/dist/engine/boot-validator/screens.d.ts +3 -1
- package/dist/engine/boot-validator/screens.js +33 -2
- package/dist/engine/boot-validator/selector-owner-panels.d.ts +2 -0
- package/dist/engine/boot-validator/selector-owner-panels.js +25 -0
- package/dist/engine/boot-validator/url-prefill-fields.js +7 -0
- package/dist/engine/build-config-feature-schema.js +31 -6
- package/dist/engine/changeset-changes.js +6 -0
- package/dist/engine/entity-handlers.js +1 -1
- package/dist/engine/extension-names.d.ts +12 -1
- package/dist/engine/extension-names.js +7 -0
- package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
- package/dist/engine/feature-ast/extractors/index.js +1 -1
- package/dist/engine/feature-ast/extractors/round5.d.ts +1 -0
- package/dist/engine/feature-ast/extractors/round5.js +5 -0
- package/dist/engine/feature-ast/parse.js +3 -1
- package/dist/engine/feature-ui-extensions.d.ts +4 -2
- package/dist/engine/feature-ui-extensions.js +40 -3
- package/dist/engine/index.d.ts +4 -3
- package/dist/engine/index.js +2 -2
- package/dist/engine/project-app-schema-for-roles.js +20 -1
- package/dist/engine/registry-facade.js +3 -0
- package/dist/engine/registry-ingest.js +2 -0
- package/dist/engine/registry-state.d.ts +2 -1
- package/dist/engine/registry-state.js +1 -0
- package/dist/engine/role-assignment.d.ts +11 -1
- package/dist/engine/role-assignment.js +64 -3
- package/dist/engine/schema-builder.d.ts +3 -1
- package/dist/engine/schema-builder.js +5 -6
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/errors/reasons.js +7 -6
- package/dist/i18n/required-surface-keys.js +19 -3
- package/dist/pipeline/dispatch-write.js +26 -2
- package/dist/pipeline/event-consumer-state.js +6 -0
- package/dist/pipeline/event-dispatcher-admin.js +36 -16
- package/dist/pipeline/event-dispatcher-delivery.d.ts +2 -0
- package/dist/pipeline/event-dispatcher-delivery.js +22 -2
- package/dist/pipeline/event-dispatcher.js +1 -0
- package/dist/ui-types/index.d.ts +1 -1
- package/package.json +4 -4
- package/src/changes.json +40 -0
package/dist/engine/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export { declareEscapeHatch } from "./escape-hatch-declaration.js";
|
|
|
28
28
|
export type { EmitCtx } from "./event-helpers.js";
|
|
29
29
|
export { emitEvent, typedPayload } from "./event-helpers.js";
|
|
30
30
|
export type { KumikoExtensionName, TenantResourceExtensionName } from "./extension-names.js";
|
|
31
|
-
export { EXT_DERIVATIVE_OVERLAY_RESOLVER, EXT_DERIVATIVE_PUBLIC_PREDICATE, EXT_DERIVATIVE_RENDERER, EXT_EXTERNAL_RESOURCE, EXT_FILE_PROVIDER, EXT_INFRA_RESOURCE, EXT_PRINCIPAL_STATUS, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, EXT_TENANT_LIFECYCLE_STATUS, EXT_USER_DATA, EXT_USER_DATA_ORDER, FILE_PROVIDER_CONFIG_KEY, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, TENANT_MEMBERSHIPS_QUERY, } from "./extension-names.js";
|
|
31
|
+
export { EXT_ASSIGNABLE_ROLE, EXT_DERIVATIVE_OVERLAY_RESOLVER, EXT_DERIVATIVE_PUBLIC_PREDICATE, EXT_DERIVATIVE_RENDERER, EXT_EXTERNAL_RESOURCE, EXT_FILE_PROVIDER, EXT_INFRA_RESOURCE, EXT_PRINCIPAL_STATUS, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, EXT_TENANT_LIFECYCLE_STATUS, EXT_USER_DATA, EXT_USER_DATA_ORDER, FILE_PROVIDER_CONFIG_KEY, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, TENANT_MEMBERSHIPS_QUERY, } from "./extension-names.js";
|
|
32
32
|
export { EXTENSION_SELECTOR_HINT_KEY, extensionSelectorTargets, SELECTED_EXTENSIONS_QUERY, selectablePluginIds, } from "./extension-selector-plugins.js";
|
|
33
33
|
export { extensionUsageEscapeHatchReason } from "./extensions/escape-hatch-usage.js";
|
|
34
34
|
export type { StorageProviderDestroyTenantHook, StorageProviderExtensionHooks, StorageProviderHookCtx, } from "./extensions/storage-provider.js";
|
|
@@ -57,7 +57,8 @@ export { readClaim } from "./read-claim.js";
|
|
|
57
57
|
export { createRegistry } from "./registry.js";
|
|
58
58
|
export type { ClampInfo, ResolveOptions } from "./resolve-config-or-param.js";
|
|
59
59
|
export { resolveConfigOrParam } from "./resolve-config-or-param.js";
|
|
60
|
-
export {
|
|
60
|
+
export type { AssignableAppRoles, AssignableFromRole } from "./role-assignment.js";
|
|
61
|
+
export { assignableAppRolesFromUsages, findForbiddenRoleAssignment, isAssignableByRole, } from "./role-assignment.js";
|
|
61
62
|
export { runsInLane } from "./run-in.js";
|
|
62
63
|
export type { StepListOutcome } from "./run-pipeline.js";
|
|
63
64
|
export { runPipeline, runStepList } from "./run-pipeline.js";
|
|
@@ -71,7 +72,7 @@ export { describeWorkflowStepError } from "./steps/describe-workflow-step-error.
|
|
|
71
72
|
export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
|
|
72
73
|
export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
|
|
73
74
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
|
|
74
|
-
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, 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";
|
|
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";
|
|
75
76
|
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, HookPhases, isAgentVisibleScreen, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
|
|
76
77
|
export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.js";
|
|
77
78
|
export { runValidation } from "./validation.js";
|
package/dist/engine/index.js
CHANGED
|
@@ -26,7 +26,7 @@ export { createEntityExecutor, defineEntityCreateHandler, defineEntityDeleteHand
|
|
|
26
26
|
defineEntityQueryHandler, defineEntityRestoreHandler, defineEntityUpdateHandler, defineEntityWriteHandler, defineProjectionQueryHandler, entityListSchema, MAX_LIST_LIMIT, registerEntityCrud, } from "./entity-handlers.js";
|
|
27
27
|
export { declareEscapeHatch } from "./escape-hatch-declaration.js";
|
|
28
28
|
export { emitEvent, typedPayload } from "./event-helpers.js";
|
|
29
|
-
export { EXT_DERIVATIVE_OVERLAY_RESOLVER, EXT_DERIVATIVE_PUBLIC_PREDICATE, EXT_DERIVATIVE_RENDERER, EXT_EXTERNAL_RESOURCE, EXT_FILE_PROVIDER, EXT_INFRA_RESOURCE, EXT_PRINCIPAL_STATUS, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, EXT_TENANT_LIFECYCLE_STATUS, EXT_USER_DATA, EXT_USER_DATA_ORDER, FILE_PROVIDER_CONFIG_KEY, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, TENANT_MEMBERSHIPS_QUERY, } from "./extension-names.js";
|
|
29
|
+
export { EXT_ASSIGNABLE_ROLE, EXT_DERIVATIVE_OVERLAY_RESOLVER, EXT_DERIVATIVE_PUBLIC_PREDICATE, EXT_DERIVATIVE_RENDERER, EXT_EXTERNAL_RESOURCE, EXT_FILE_PROVIDER, EXT_INFRA_RESOURCE, EXT_PRINCIPAL_STATUS, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, EXT_TENANT_LIFECYCLE_STATUS, EXT_USER_DATA, EXT_USER_DATA_ORDER, FILE_PROVIDER_CONFIG_KEY, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, TENANT_MEMBERSHIPS_QUERY, } from "./extension-names.js";
|
|
30
30
|
export { EXTENSION_SELECTOR_HINT_KEY, extensionSelectorTargets, SELECTED_EXTENSIONS_QUERY, selectablePluginIds, } from "./extension-selector-plugins.js";
|
|
31
31
|
export { extensionUsageEscapeHatchReason } from "./extensions/escape-hatch-usage.js";
|
|
32
32
|
export { isTenantDataExtensionHooks, } from "./extensions/tenant-data.js";
|
|
@@ -54,7 +54,7 @@ export { isValidQn, parseQn, QnTypes, qn, toKebab } from "./qualified-name.js";
|
|
|
54
54
|
export { readClaim } from "./read-claim.js";
|
|
55
55
|
export { createRegistry } from "./registry.js";
|
|
56
56
|
export { resolveConfigOrParam } from "./resolve-config-or-param.js";
|
|
57
|
-
export { findForbiddenRoleAssignment } from "./role-assignment.js";
|
|
57
|
+
export { assignableAppRolesFromUsages, findForbiddenRoleAssignment, isAssignableByRole, } from "./role-assignment.js";
|
|
58
58
|
export { runsInLane } from "./run-in.js";
|
|
59
59
|
export { runPipeline, runStepList } from "./run-pipeline.js";
|
|
60
60
|
export { buildInsertSchema, buildUpdateSchema, fieldToZod } from "./schema-builder.js";
|
|
@@ -401,6 +401,22 @@ function projectMetrics(metrics, indices, keptScreenQns) {
|
|
|
401
401
|
});
|
|
402
402
|
return changed ? next : metrics;
|
|
403
403
|
}
|
|
404
|
+
function projectHeader(header, indices, keptScreenQns) {
|
|
405
|
+
const subtitle = header?.subtitle;
|
|
406
|
+
if (header === undefined || typeof subtitle === "string" || subtitle === undefined) {
|
|
407
|
+
return header;
|
|
408
|
+
}
|
|
409
|
+
let changed = false;
|
|
410
|
+
const nextParts = subtitle.map((part) => {
|
|
411
|
+
if (typeof part === "string" || part.navigate === undefined)
|
|
412
|
+
return part;
|
|
413
|
+
if (keepMetricNavigate(part.navigate, indices, keptScreenQns))
|
|
414
|
+
return part;
|
|
415
|
+
changed = true;
|
|
416
|
+
return part.field;
|
|
417
|
+
});
|
|
418
|
+
return changed ? { ...header, subtitle: nextParts } : header;
|
|
419
|
+
}
|
|
404
420
|
function projectDashboardPanels(panels, feature, keptScreenQns) {
|
|
405
421
|
const kept = panels.filter((p) => p.kind !== "screen" || isScreenTargetKept(feature.featureName, p.screen, keptScreenQns));
|
|
406
422
|
return kept.length === panels.length ? panels : kept;
|
|
@@ -451,12 +467,14 @@ function projectListScreen(original, feature, indices, keptScreenQns) {
|
|
|
451
467
|
};
|
|
452
468
|
}
|
|
453
469
|
function projectProjectionDetailScreen(screen, feature, indices, keptScreenQns) {
|
|
454
|
-
const { metrics, actions, layout, listScreenId, ...rest } = screen;
|
|
470
|
+
const { metrics, header, actions, layout, listScreenId, ...rest } = screen;
|
|
455
471
|
const nextMetrics = projectMetrics(metrics, indices, keptScreenQns);
|
|
472
|
+
const nextHeader = projectHeader(header, indices, keptScreenQns);
|
|
456
473
|
const nextActions = projectRowActionArray(actions, feature, indices, keptScreenQns);
|
|
457
474
|
const nextLayout = projectEditLayout(layout, feature, indices, keptScreenQns);
|
|
458
475
|
const nextListScreenId = projectListScreenId(listScreenId, indices, keptScreenQns);
|
|
459
476
|
const unchanged = nextMetrics === metrics &&
|
|
477
|
+
nextHeader === header &&
|
|
460
478
|
nextActions === actions &&
|
|
461
479
|
nextLayout === layout &&
|
|
462
480
|
nextListScreenId === listScreenId;
|
|
@@ -465,6 +483,7 @@ function projectProjectionDetailScreen(screen, feature, indices, keptScreenQns)
|
|
|
465
483
|
return {
|
|
466
484
|
...rest,
|
|
467
485
|
layout: nextLayout,
|
|
486
|
+
...(nextHeader !== undefined && { header: nextHeader }),
|
|
468
487
|
...(nextMetrics !== undefined && { metrics: nextMetrics }),
|
|
469
488
|
...(nextActions !== undefined && { actions: nextActions }),
|
|
470
489
|
...(nextListScreenId !== undefined && { listScreenId: nextListScreenId }),
|
|
@@ -149,6 +149,9 @@ export function buildRegistryFacade(state) {
|
|
|
149
149
|
getAllExtensionSelectors() {
|
|
150
150
|
return state.extensionSelectorMap;
|
|
151
151
|
},
|
|
152
|
+
getExtensionSelectorPanels(extensionName) {
|
|
153
|
+
return state.extensionSelectorPanelsMap.get(extensionName) ?? [];
|
|
154
|
+
},
|
|
152
155
|
getAllNotifications() {
|
|
153
156
|
return state.notificationMap;
|
|
154
157
|
},
|
|
@@ -189,6 +189,8 @@ export function populateExtensionsAndSeeds(state, feature) {
|
|
|
189
189
|
`(feature "${feature.name}") — one owning feature declares the selector.`);
|
|
190
190
|
}
|
|
191
191
|
state.extensionSelectorMap.set(sel.extensionName, sel.qualifiedKey);
|
|
192
|
+
if (sel.panels !== undefined)
|
|
193
|
+
state.extensionSelectorPanelsMap.set(sel.extensionName, sel.panels);
|
|
192
194
|
}
|
|
193
195
|
state.allReferenceData.push(...(feature.referenceData ?? []));
|
|
194
196
|
state.allConfigSeeds.push(...(feature.configSeeds ?? []));
|
|
@@ -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, 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, StoreTableDef, StreamHandlerDef, TranslationValue, TreeActionDef, WorkspaceDefinition, WriteHandlerDef } from "./types/index.js";
|
|
3
3
|
export type IncomingRelation = {
|
|
4
4
|
sourceEntity: string;
|
|
5
5
|
relationName: string;
|
|
@@ -46,6 +46,7 @@ export type RegistryState = {
|
|
|
46
46
|
extensionMap: Map<string, RegistrarExtensionDef>;
|
|
47
47
|
extensionUsages: RegistrarExtensionRegistration[];
|
|
48
48
|
extensionSelectorMap: Map<string, string>;
|
|
49
|
+
extensionSelectorPanelsMap: Map<string, readonly ExtensionSelectorPanel[]>;
|
|
49
50
|
allReferenceData: ReferenceDataDef[];
|
|
50
51
|
allConfigSeeds: ConfigSeedDef[];
|
|
51
52
|
mergedTranslations: Record<string, Record<string, TranslationValue>>;
|
|
@@ -1 +1,11 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type AssignableFromRole = "User" | "Member" | "Editor" | "Admin" | "TenantAdmin" | "SystemAdmin";
|
|
2
|
+
/** App role → lowest built-in role allowed to assign it. */
|
|
3
|
+
export type AssignableAppRoles = ReadonlyMap<string, AssignableFromRole>;
|
|
4
|
+
export declare const DEFAULT_APP_ROLE_ASSIGNABLE_FROM: AssignableFromRole;
|
|
5
|
+
export declare function assignableAppRolesFromUsages(usages: readonly {
|
|
6
|
+
entityName: string;
|
|
7
|
+
options?: Record<string, unknown> | undefined;
|
|
8
|
+
}[]): AssignableAppRoles;
|
|
9
|
+
/** True when `actorRole` ranks at least as high as the role an app role is assignable from. */
|
|
10
|
+
export declare function isAssignableByRole(assignableFrom: AssignableFromRole, actorRole: AssignableFromRole): boolean;
|
|
11
|
+
export declare function findForbiddenRoleAssignment(actorRoles: readonly string[], assignedRoles: readonly string[], targetCurrentRoles: readonly string[], assignableAppRoles?: AssignableAppRoles): string | undefined;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { ROLES } from "../auth/roles.js";
|
|
2
|
+
import { isForbiddenMembershipRole } from "./membership-roles.js";
|
|
3
|
+
const PLATFORM_ROLE_NAMES = new Set(Object.values(ROLES));
|
|
1
4
|
// Prototype-free rank table — Object literals leak Object.prototype
|
|
2
5
|
// (`constructor`, `toString`, …) into `ROLE_RANKS[role]` lookups and turn
|
|
3
6
|
// Math.max into NaN, which fails every `rank > actorRank` check open.
|
|
4
7
|
const ROLE_RANKS = new Map([
|
|
5
8
|
["User", 0],
|
|
9
|
+
// Default seed role — unranked it would fail closed and never be assignable.
|
|
10
|
+
["Member", 0],
|
|
6
11
|
// Matches DEFAULT_INVITE_ROLE_OPTIONS — must stay ranked or invite UI fails closed.
|
|
7
12
|
["Editor", 1],
|
|
8
13
|
["Admin", 2],
|
|
@@ -10,7 +15,55 @@ const ROLE_RANKS = new Map([
|
|
|
10
15
|
["SystemAdmin", 4],
|
|
11
16
|
["system", 5],
|
|
12
17
|
]);
|
|
13
|
-
//
|
|
18
|
+
// App roles are ordinary, non-privileged tiers: whoever may manage members
|
|
19
|
+
// (access.admin) may grant them; an app raises the bar via assignableFrom.
|
|
20
|
+
export const DEFAULT_APP_ROLE_ASSIGNABLE_FROM = "Admin";
|
|
21
|
+
const NO_ASSIGNABLE_APP_ROLES = new Map();
|
|
22
|
+
const ASSIGNABLE_FROM_ROLES = new Set([
|
|
23
|
+
"User",
|
|
24
|
+
"Member",
|
|
25
|
+
"Editor",
|
|
26
|
+
"Admin",
|
|
27
|
+
"TenantAdmin",
|
|
28
|
+
"SystemAdmin",
|
|
29
|
+
]);
|
|
30
|
+
function isAssignableFromRole(value) {
|
|
31
|
+
return typeof value === "string" && ASSIGNABLE_FROM_ROLES.has(value);
|
|
32
|
+
}
|
|
33
|
+
export function assignableAppRolesFromUsages(usages) {
|
|
34
|
+
const result = new Map();
|
|
35
|
+
for (const usage of usages) {
|
|
36
|
+
const role = usage.entityName;
|
|
37
|
+
if (role === "") {
|
|
38
|
+
throw new Error("assignableRole: role name must not be empty");
|
|
39
|
+
}
|
|
40
|
+
// Platform roles (TenantOwner, DataProtectionOfficer) gate handlers that
|
|
41
|
+
// TenantAdmin/Admin cannot reach — declaring one would let an Admin
|
|
42
|
+
// self-grant it, so they stay out of reach of app declarations.
|
|
43
|
+
if (ROLE_RANKS.has(role) || PLATFORM_ROLE_NAMES.has(role)) {
|
|
44
|
+
throw new Error(`assignableRole: "${role}" is a built-in role and cannot be redeclared`);
|
|
45
|
+
}
|
|
46
|
+
if (isForbiddenMembershipRole(role)) {
|
|
47
|
+
throw new Error(`assignableRole: "${role}" is a reserved role and cannot be a membership role`);
|
|
48
|
+
}
|
|
49
|
+
const declaredAssignableFrom = usage.options?.["assignableFrom"];
|
|
50
|
+
if (declaredAssignableFrom !== undefined && !isAssignableFromRole(declaredAssignableFrom)) {
|
|
51
|
+
throw new Error(`assignableRole: "${role}" has invalid assignableFrom, expected one of ${[...ASSIGNABLE_FROM_ROLES].join(", ")}`);
|
|
52
|
+
}
|
|
53
|
+
const assignableFrom = declaredAssignableFrom ?? DEFAULT_APP_ROLE_ASSIGNABLE_FROM;
|
|
54
|
+
const existing = result.get(role);
|
|
55
|
+
if (existing !== undefined && existing !== assignableFrom) {
|
|
56
|
+
throw new Error(`assignableRole: "${role}" is declared with conflicting assignableFrom (${existing} vs ${assignableFrom})`);
|
|
57
|
+
}
|
|
58
|
+
result.set(role, assignableFrom);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
/** True when `actorRole` ranks at least as high as the role an app role is assignable from. */
|
|
63
|
+
export function isAssignableByRole(assignableFrom, actorRole) {
|
|
64
|
+
return getRoleRank(assignableFrom) <= getRoleRank(actorRole);
|
|
65
|
+
}
|
|
66
|
+
// Unknown roles: +∞ on the assigned path (cannot grant what we don't know, unless declared),
|
|
14
67
|
// -1 on the actor path (cannot elevate via an unrecognized self-role).
|
|
15
68
|
function roleRankOr(role, unknownRank) {
|
|
16
69
|
return ROLE_RANKS.get(role) ?? unknownRank;
|
|
@@ -27,15 +80,23 @@ function maxRoleRank(roles) {
|
|
|
27
80
|
function knownRoleRank(role) {
|
|
28
81
|
return ROLE_RANKS.get(role);
|
|
29
82
|
}
|
|
83
|
+
// Built-ins win: a declaration can never re-tier a built-in role.
|
|
84
|
+
function requiredAssignRank(role, assignableAppRoles) {
|
|
85
|
+
const builtIn = ROLE_RANKS.get(role);
|
|
86
|
+
if (builtIn !== undefined)
|
|
87
|
+
return builtIn;
|
|
88
|
+
const assignableFrom = assignableAppRoles.get(role);
|
|
89
|
+
return assignableFrom === undefined ? Number.POSITIVE_INFINITY : getRoleRank(assignableFrom);
|
|
90
|
+
}
|
|
30
91
|
export function findForbiddenRoleAssignment(actorRoles, assignedRoles,
|
|
31
92
|
// Required so callers cannot accidentally disable the downgrade/
|
|
32
93
|
// takeover guard by omitting the third argument (new users: pass []).
|
|
33
|
-
targetCurrentRoles) {
|
|
94
|
+
targetCurrentRoles, assignableAppRoles = NO_ASSIGNABLE_APP_ROLES) {
|
|
34
95
|
const actorRank = maxRoleRank(actorRoles);
|
|
35
96
|
// Assign path: fail-closed on unknown / above-actor roles — except unranked
|
|
36
97
|
// app roles the target already holds (round-trip restore after strip).
|
|
37
98
|
const forbiddenAssigned = assignedRoles.find((role) => {
|
|
38
|
-
if (
|
|
99
|
+
if (requiredAssignRank(role, assignableAppRoles) <= actorRank)
|
|
39
100
|
return false;
|
|
40
101
|
if (knownRoleRank(role) === undefined && targetCurrentRoles.includes(role))
|
|
41
102
|
return false;
|
|
@@ -4,5 +4,7 @@ export declare function isRepresentableAtScale(n: number, scale: number): boolea
|
|
|
4
4
|
export declare function fieldToZod(field: FieldDefinition, currencies: readonly string[], opts?: {
|
|
5
5
|
readonly applyDefaults?: boolean;
|
|
6
6
|
}): z.ZodTypeAny;
|
|
7
|
-
export declare function buildInsertSchema(entity: EntityDefinition, currencies?: readonly string[], excludedFields?: readonly string[]
|
|
7
|
+
export declare function buildInsertSchema(entity: EntityDefinition, currencies?: readonly string[], excludedFields?: readonly string[], options?: {
|
|
8
|
+
readonly allowCallerId?: boolean;
|
|
9
|
+
}): z.ZodObject<Record<string, z.ZodTypeAny>>;
|
|
8
10
|
export declare function buildUpdateSchema(entity: EntityDefinition, currencies?: readonly string[], excludedFields?: readonly string[]): z.ZodObject<Record<string, z.ZodTypeAny>>;
|
|
@@ -366,7 +366,7 @@ function applyTotalsMatchRefinements(entity, schema) {
|
|
|
366
366
|
function notWritableField() {
|
|
367
367
|
return z.never().optional();
|
|
368
368
|
}
|
|
369
|
-
export function buildInsertSchema(entity, currencies = [...DEFAULT_CURRENCIES], excludedFields = []) {
|
|
369
|
+
export function buildInsertSchema(entity, currencies = [...DEFAULT_CURRENCIES], excludedFields = [], options = {}) {
|
|
370
370
|
const shape = {};
|
|
371
371
|
for (const [name, field] of Object.entries(entity.fields)) {
|
|
372
372
|
if (excludedFields.includes(name)) {
|
|
@@ -378,11 +378,10 @@ export function buildInsertSchema(entity, currencies = [...DEFAULT_CURRENCIES],
|
|
|
378
378
|
const isRequired = "required" in field && field.required === true;
|
|
379
379
|
shape[name] = isRequired || hasDefault ? zodField : zodField.optional();
|
|
380
380
|
}
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
shape["id"] = z.uuid().optional();
|
|
381
|
+
// Opt-in only for the generic create verb, which honors the id for system identities;
|
|
382
|
+
// other schema consumers must keep stripping an unknown `id`.
|
|
383
|
+
if (options.allowCallerId)
|
|
384
|
+
shape["id"] = z.uuid().optional();
|
|
386
385
|
return applyTotalsMatchRefinements(entity, z.object(shape));
|
|
387
386
|
}
|
|
388
387
|
export function buildUpdateSchema(entity, currencies = [...DEFAULT_CURRENCIES], excludedFields = []) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { isAgentVisibleScreen, resolveAgentExposure, } from "@cosmicdrift/kumiko-types/agent-exposure";
|
|
2
|
-
export type { ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigBounds, ConfigCascade, ConfigCascadeLevel, ConfigComputedContext, ConfigComputedFn, ConfigDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValue, ConfigValueSource, ConfigValueWithSource, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, ExtensionSelectorDef, JobBackoff, JobBackoffStrategy, JobDefinition, JobHandlerFn, JobRunIn, JobTrigger, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, PluralForms, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionHooks, RegistrarExtensionRegistration, RunIn, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, UiExtensionDef, } from "@cosmicdrift/kumiko-types/config";
|
|
2
|
+
export type { ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigBounds, ConfigCascade, ConfigCascadeLevel, ConfigComputedContext, ConfigComputedFn, ConfigDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValue, ConfigValueSource, ConfigValueWithSource, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, ExtensionSelectorDef, ExtensionSelectorPanel, JobBackoff, JobBackoffStrategy, JobDefinition, JobHandlerFn, JobRunIn, JobTrigger, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, PluralForms, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionHooks, RegistrarExtensionRegistration, RunIn, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, UiExtensionDef, } from "@cosmicdrift/kumiko-types/config";
|
|
3
3
|
export type { QueryHandlerDefinition, StreamHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput, } from "@cosmicdrift/kumiko-types/define-handler";
|
|
4
4
|
export type { EntityCrudRegistrar, EntityCrudVerb, EntityHandlerOptions, EntityQueryHandlerOptions, EntityWriteHandlerOptions, RegisterEntityCrudOptions, } from "@cosmicdrift/kumiko-types/entity-handlers";
|
|
5
5
|
export type { KumikoEntityTypeMap, KumikoEventTypeMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, } from "@cosmicdrift/kumiko-types/event-type-map";
|
|
@@ -17,7 +17,7 @@ export type { ContentCollectionDefinition, NavDefinition, NavIconKey, } from "@c
|
|
|
17
17
|
export type { FromRule, FromRuleKind, OwnershipClause, OwnershipMap, OwnershipRef, OwnershipRule, SqlFragment, WhereRule, WhereRuleContext, } from "@cosmicdrift/kumiko-types/ownership";
|
|
18
18
|
export type { EntityProjectionExtension, MspErrorMode, MspErrorPolicy, MultiStreamApplyFn, MultiStreamProjectionDefinition, ProjectionDefinition, ProjectionTable, SingleStreamApplyFn, } from "@cosmicdrift/kumiko-types/projection";
|
|
19
19
|
export type { BelongsToRelation, EntityRelations, HasManyRelation, ManyToManyRelation, RelationDefinition, } from "@cosmicdrift/kumiko-types/relations";
|
|
20
|
-
export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
20
|
+
export type { ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListExpandableRow, EntityListScreenDefinition, FieldCondition, FieldFormatRegistry, FieldRenderer, FormatSpec, FormWidth, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RecordHeaderSpec, RecordHeaderSubtitlePart, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, UnitKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
21
21
|
export { metricField } from "@cosmicdrift/kumiko-types/screen";
|
|
22
22
|
export type { TargetRef } from "@cosmicdrift/kumiko-types/target-ref";
|
|
23
23
|
export type { Subscribe, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, } from "@cosmicdrift/kumiko-types/tree-node";
|
package/dist/errors/reasons.js
CHANGED
|
@@ -66,16 +66,17 @@ export const FrameworkReasons = {
|
|
|
66
66
|
// sister catalog to FrameworkReasons instead of extending it — see the
|
|
67
67
|
// file-header note above for why these carry the `agent.` prefix.
|
|
68
68
|
export const AgentReasons = {
|
|
69
|
-
//
|
|
70
|
-
// catalog
|
|
69
|
+
// Error turn in the agent loop when the model requests a tool outside the
|
|
70
|
+
// run's catalog; UnprocessableError in rule validation (handler missing from
|
|
71
|
+
// the caller's role-filtered manifest, or `ai-agent-edit` not mounted).
|
|
71
72
|
toolNotAllowed: "agent.tool_not_allowed",
|
|
72
|
-
//
|
|
73
|
-
//
|
|
73
|
+
// Not thrown: the run ends with a `clarify` turn (`payload.reason`) once
|
|
74
|
+
// MAX_AGENT_ROUNDS is reached.
|
|
74
75
|
iterationLimit: "agent.iteration_limit",
|
|
75
76
|
// AccessDeniedError: the tool exists, but the caller's roles don't cover
|
|
76
77
|
// the handler behind it.
|
|
77
78
|
permissionDenied: "agent.permission_denied",
|
|
78
|
-
//
|
|
79
|
-
//
|
|
79
|
+
// UnprocessableError: a permission rule with `always` was requested for an
|
|
80
|
+
// `agent: { risk: "high" }` handler; `details.handlerQn` names it.
|
|
80
81
|
highRiskNoAlways: "agent.high_risk_no_always",
|
|
81
82
|
};
|
|
@@ -42,6 +42,7 @@ function editFieldName(f) {
|
|
|
42
42
|
// Group titles are translated exactly like the section title (computeEditViewModel).
|
|
43
43
|
function pushSectionTitles(out, section, treatAsKey = false) {
|
|
44
44
|
pushKey(out, section.title, treatAsKey);
|
|
45
|
+
pushKey(out, section.subtitle, treatAsKey);
|
|
45
46
|
for (const group of section.groups ?? [])
|
|
46
47
|
pushKey(out, group.title, treatAsKey);
|
|
47
48
|
}
|
|
@@ -51,6 +52,10 @@ function pushRowActionKeys(out, action) {
|
|
|
51
52
|
pushKey(out, action.confirm);
|
|
52
53
|
pushKey(out, action.confirmLabel);
|
|
53
54
|
}
|
|
55
|
+
if (action.kind === "drawer") {
|
|
56
|
+
pushKey(out, action.title);
|
|
57
|
+
pushKey(out, action.subtitle);
|
|
58
|
+
}
|
|
54
59
|
}
|
|
55
60
|
function pushDashboardScreenKeys(out, dashboard) {
|
|
56
61
|
for (const panel of dashboard.panels)
|
|
@@ -103,9 +108,12 @@ function pushToolbarActionKeys(out, action) {
|
|
|
103
108
|
pushKey(out, action.confirm);
|
|
104
109
|
pushKey(out, action.confirmLabel);
|
|
105
110
|
}
|
|
106
|
-
// drawer-kind carries no confirm/confirmLabel — action.label
|
|
107
|
-
// toolbar button
|
|
108
|
-
|
|
111
|
+
// drawer-kind carries no confirm/confirmLabel — action.label is the
|
|
112
|
+
// toolbar button and the Drawer title unless `title` overrides it.
|
|
113
|
+
if (action.kind === "drawer") {
|
|
114
|
+
pushKey(out, action.title);
|
|
115
|
+
pushKey(out, action.subtitle);
|
|
116
|
+
}
|
|
109
117
|
}
|
|
110
118
|
function pushRelatedListSectionKeys(out, section) {
|
|
111
119
|
pushKey(out, section.title);
|
|
@@ -180,6 +188,7 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
|
180
188
|
case "entityEdit": {
|
|
181
189
|
const edit = screen;
|
|
182
190
|
pushKey(out, edit.submitLabel);
|
|
191
|
+
pushKey(out, edit.titleTemplate);
|
|
183
192
|
for (const section of edit.layout.sections) {
|
|
184
193
|
if (isExtensionEditSection(section)) {
|
|
185
194
|
pushKey(out, section.title);
|
|
@@ -204,6 +213,8 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
|
204
213
|
pushKey(out, form.submitLabel);
|
|
205
214
|
pushKey(out, form.summary?.title);
|
|
206
215
|
pushKey(out, form.summary?.subtitle);
|
|
216
|
+
for (const action of form.footerActions ?? [])
|
|
217
|
+
pushKey(out, action.label);
|
|
207
218
|
for (const fieldName of Object.keys(form.fields)) {
|
|
208
219
|
const override = form.fieldLabels?.[fieldName];
|
|
209
220
|
if (override !== undefined)
|
|
@@ -328,6 +339,11 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
|
328
339
|
}
|
|
329
340
|
if (section.kind === "relatedList") {
|
|
330
341
|
pushRelatedListSectionKeys(out, section);
|
|
342
|
+
if (section.groupBy !== undefined) {
|
|
343
|
+
pushKey(out, section.groupBy.label);
|
|
344
|
+
for (const label of Object.values(section.groupBy.labels ?? {}))
|
|
345
|
+
pushKey(out, label);
|
|
346
|
+
}
|
|
331
347
|
continue;
|
|
332
348
|
}
|
|
333
349
|
if (isWriteFormEditSection(section)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { selectRowForUpdateById } from "../db/queries/entity-read.js";
|
|
2
2
|
import { asEntityTableMeta, selectMany } from "../db/query.js";
|
|
3
3
|
import { buildEntityTable, toSnakeCase } from "../db/table-builder.js";
|
|
4
|
+
import { createTenantDb, hasTenantColumn } from "../db/tenant-db.js";
|
|
4
5
|
import { tenantDbRunner } from "../db/tenant-db-runner.js";
|
|
5
6
|
import { hasAccess } from "../engine/access.js";
|
|
6
7
|
import { ConfigScopes } from "../engine/constants.js";
|
|
@@ -126,6 +127,24 @@ function isForeignTenantParentRow(parentRow, user) {
|
|
|
126
127
|
const rowTenantId = parentRow["tenantId"];
|
|
127
128
|
return typeof rowTenantId === "string" && rowTenantId !== user.tenantId;
|
|
128
129
|
}
|
|
130
|
+
// A custom create handler may return a row without a string tenantId; the row's
|
|
131
|
+
// tenant is then unknown from the payload, so re-read it through the caller's tenant
|
|
132
|
+
// filter. Fail-closed: anything that cannot be verified counts as not visible.
|
|
133
|
+
// Tenant reads also return SYSTEM reference rows, so the row's own tenantId must match
|
|
134
|
+
// (same rule as the string path) unless the entity has no tenant column.
|
|
135
|
+
async function isParentRowHiddenFromCaller(ctx, parentEntityName, parentId, user, tx) {
|
|
136
|
+
if (parentEntityName === undefined)
|
|
137
|
+
return true;
|
|
138
|
+
const table = getTable(ctx, parentEntityName);
|
|
139
|
+
const source = resolveDbSource(ctx, tx);
|
|
140
|
+
if (!table || !source)
|
|
141
|
+
return true;
|
|
142
|
+
const callerDb = createTenantDb(source, user.tenantId, "tenant");
|
|
143
|
+
const visibleRow = await callerDb.fetchOne(table, { id: parentId });
|
|
144
|
+
if (visibleRow === undefined)
|
|
145
|
+
return true;
|
|
146
|
+
return hasTenantColumn(table) && visibleRow["tenantId"] !== user.tenantId;
|
|
147
|
+
}
|
|
129
148
|
// Nested-write orchestration (v1: depth=1, create-only, hasMany-only).
|
|
130
149
|
//
|
|
131
150
|
// When a parent `:create` handler's payload carries values under keys
|
|
@@ -199,13 +218,18 @@ export async function executeNestedWrite(ctx, type, payload, user, origin, tx, a
|
|
|
199
218
|
}));
|
|
200
219
|
}
|
|
201
220
|
// A custom create handler may return an existing foreign row; verify it before attaching children.
|
|
202
|
-
|
|
221
|
+
const parentEntityName = registry.getHandlerEntity(type);
|
|
222
|
+
const isCallerScoped = !registry.isHandlerSystemScoped(type);
|
|
223
|
+
const isParentForeign = isCallerScoped &&
|
|
224
|
+
(typeof parentRow["tenantId"] === "string"
|
|
225
|
+
? isForeignTenantParentRow(parentRow, user)
|
|
226
|
+
: await isParentRowHiddenFromCaller(ctx, parentEntityName, parentId, user, tx));
|
|
227
|
+
if (isParentForeign) {
|
|
203
228
|
return writeFailure(new AccessDeniedError({
|
|
204
229
|
message: `nested-write: parent row belongs to another tenant — refusing to attach children to "${type}"`,
|
|
205
230
|
details: { reason: FrameworkReasons.fieldAccessDenied, handler: type },
|
|
206
231
|
}));
|
|
207
232
|
}
|
|
208
|
-
const parentEntityName = registry.getHandlerEntity(type);
|
|
209
233
|
if (parentEntityName) {
|
|
210
234
|
const parentEntity = registry.getEntity(parentEntityName);
|
|
211
235
|
if (parentEntity) {
|
|
@@ -29,6 +29,10 @@ export const eventConsumerStateTable = pgTable("kumiko_event_consumers", {
|
|
|
29
29
|
// skipPoisonEvent() — an operator vouching the consumer is healthy.
|
|
30
30
|
rearmCount: integer("rearm_count").notNull().default(0),
|
|
31
31
|
pendingGaps: jsonb("pending_gaps").$type().default([]).notNull(),
|
|
32
|
+
// Id of the event whose handler failed last (halt-on-poison). skipPoisonEvent
|
|
33
|
+
// skips exactly this id; guessing the smallest visible id would drop a healthy
|
|
34
|
+
// writer that committed late into a pending gap. Null once a delivery succeeds.
|
|
35
|
+
lastFailedEventId: bigint("last_failed_event_id", { mode: "bigint" }),
|
|
32
36
|
lastError: text("last_error"),
|
|
33
37
|
updatedAt: instant("updated_at", { precision: 3 }).notNull().default(sql `now()`),
|
|
34
38
|
}, (t) => ({
|
|
@@ -78,6 +82,8 @@ export async function createEventConsumerStateTable(db) {
|
|
|
78
82
|
/* ifNotExists */ true);
|
|
79
83
|
await alterTableAddColumn(db, "kumiko_event_consumers", "pending_gaps", "jsonb", " DEFAULT '[]'::jsonb", " NOT NULL",
|
|
80
84
|
/* ifNotExists */ true);
|
|
85
|
+
await alterTableAddColumn(db, "kumiko_event_consumers", "last_failed_event_id", "bigint", "", "",
|
|
86
|
+
/* ifNotExists */ true);
|
|
81
87
|
// Runs on every boot, including mid-rolling-deploy: a new pod can delete
|
|
82
88
|
// an old pod's still-in-use per-instance row out from under it. That old
|
|
83
89
|
// pod's next acquireConsumer then finds no row, delivers no more SSE/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { advanceConsumerPastEventReturning, removePendingGapReturning, updateConsumerStatusReturning, } from "../db/queries/event-consumer.js";
|
|
1
|
+
import { advanceConsumerPastEventReturning, removePendingGapReturning, selectSnapshotXmax, updateConsumerStatusReturning, } from "../db/queries/event-consumer.js";
|
|
2
2
|
import { selectNextEventIdAfter, selectSmallestVisibleIdInRanges, } from "../db/queries/event-store.js";
|
|
3
3
|
import { coerceRow, extractTableInfo, selectMany } from "../db/query.js";
|
|
4
4
|
import { getEventsHighWaterMark } from "../event-store/index.js";
|
|
5
|
-
import { eventConsumerStateTable, SHARED_INSTANCE_SENTINEL } from "./event-consumer-state.js";
|
|
5
|
+
import { eventConsumerStateTable, SHARED_INSTANCE_SENTINEL, } from "./event-consumer-state.js";
|
|
6
6
|
import { rangeContainsId, splitRangeExcludingIds, toIdRanges } from "./pending-gap-ranges.js";
|
|
7
7
|
// --- Ops recovery surface ---
|
|
8
8
|
//
|
|
@@ -76,29 +76,38 @@ export async function enableConsumer(db, name, instanceId = SHARED_INSTANCE_SENT
|
|
|
76
76
|
}
|
|
77
77
|
return applyConsumerStatusTransition(db, name, instanceId, "idle");
|
|
78
78
|
}
|
|
79
|
-
// skipPoisonEvent
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// TX so concurrent dispatcher passes can't
|
|
85
|
-
// idempotent no-op.
|
|
79
|
+
// skipPoisonEvent skips the event whose handler failed last
|
|
80
|
+
// (last_failed_event_id, set by the delivery's halt-on-poison branch): inside
|
|
81
|
+
// pending_gaps it is split out of its range, otherwise the cursor moves onto
|
|
82
|
+
// it. Without a recorded id (consumer parked before that column existed) it
|
|
83
|
+
// falls back to a guess: the smallest visible pending-gap id, else the next
|
|
84
|
+
// event after the cursor. Single TX so concurrent dispatcher passes can't
|
|
85
|
+
// double-advance. Nothing to skip → idempotent no-op.
|
|
86
86
|
export async function skipPoisonEvent(db, name, instanceId = SHARED_INSTANCE_SENTINEL) {
|
|
87
87
|
const before = await requireConsumerRow(db, name, instanceId);
|
|
88
88
|
return db.begin(async (tx) => {
|
|
89
89
|
const pendingGaps = before.pendingGaps;
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
const failedEventId = before.lastFailedEventId;
|
|
91
|
+
const failedIsPending = failedEventId !== null && pendingGaps.some((gap) => rangeContainsId(gap, failedEventId));
|
|
92
|
+
const failedIsAboveCursor = failedEventId !== null && failedEventId > before.lastProcessedEventId;
|
|
93
|
+
const pendingPoisonId = failedIsPending
|
|
94
|
+
? failedEventId
|
|
95
|
+
: failedIsAboveCursor
|
|
96
|
+
? null
|
|
97
|
+
: await selectSmallestVisibleIdInRanges(tx, toIdRanges(pendingGaps));
|
|
98
|
+
if (pendingPoisonId !== null) {
|
|
99
|
+
const newPendingGaps = pendingGaps.flatMap((gap) => rangeContainsId(gap, pendingPoisonId)
|
|
100
|
+
? splitRangeExcludingIds(gap, [pendingPoisonId])
|
|
94
101
|
: [gap]);
|
|
95
102
|
const raw = await removePendingGapReturning(tx, name, instanceId, newPendingGaps);
|
|
96
103
|
const updated = raw && coerceRow(raw, extractTableInfo(eventConsumerStateTable));
|
|
97
104
|
if (!updated)
|
|
98
105
|
throw new Error(`Consumer "${name}" (instance_id="${instanceId}") vanished mid-skip — retry.`);
|
|
99
|
-
return { ...normalizeConsumerState(updated), skippedEventId:
|
|
106
|
+
return { ...normalizeConsumerState(updated), skippedEventId: pendingPoisonId };
|
|
100
107
|
}
|
|
101
|
-
const poisonId =
|
|
108
|
+
const poisonId = failedIsAboveCursor
|
|
109
|
+
? failedEventId
|
|
110
|
+
: await selectNextEventIdAfter(tx, before.lastProcessedEventId);
|
|
102
111
|
if (poisonId === null) {
|
|
103
112
|
const [unchanged] = await selectMany(tx, eventConsumerStateTable, {
|
|
104
113
|
name,
|
|
@@ -108,7 +117,18 @@ export async function skipPoisonEvent(db, name, instanceId = SHARED_INSTANCE_SEN
|
|
|
108
117
|
throw new Error(`Consumer "${name}" (instance_id="${instanceId}") vanished — retry.`);
|
|
109
118
|
return { ...normalizeConsumerState(unchanged), skippedEventId: null };
|
|
110
119
|
}
|
|
111
|
-
|
|
120
|
+
// Delivery stops at the poison event, so ids between the cursor and it were
|
|
121
|
+
// invisible during that pass (in-flight commits). Jumping the cursor over them
|
|
122
|
+
// would lose them; keep them as a pending gap like the dispatcher's own gaps.
|
|
123
|
+
const skippedWindowStart = before.lastProcessedEventId + 1n;
|
|
124
|
+
const gapToAdd = poisonId > skippedWindowStart
|
|
125
|
+
? {
|
|
126
|
+
from: skippedWindowStart.toString(),
|
|
127
|
+
to: (poisonId - 1n).toString(),
|
|
128
|
+
xmax: await selectSnapshotXmax(tx),
|
|
129
|
+
}
|
|
130
|
+
: null;
|
|
131
|
+
const raw = await advanceConsumerPastEventReturning(tx, name, instanceId, poisonId, gapToAdd);
|
|
112
132
|
const updated = raw && coerceRow(raw, extractTableInfo(eventConsumerStateTable));
|
|
113
133
|
if (!updated)
|
|
114
134
|
throw new Error(`Consumer "${name}" (instance_id="${instanceId}") vanished mid-skip — retry.`);
|
|
@@ -13,6 +13,7 @@ export type ConsumerStateRowShape = {
|
|
|
13
13
|
readonly attempts: number;
|
|
14
14
|
readonly rearmCount: number;
|
|
15
15
|
readonly pendingGaps: readonly PendingGapEntry[];
|
|
16
|
+
readonly lastFailedEventId: bigint | null;
|
|
16
17
|
readonly lastError: string | null;
|
|
17
18
|
readonly updatedAt: Temporal.Instant;
|
|
18
19
|
};
|
|
@@ -58,6 +59,7 @@ export type DeliveryOutcome = {
|
|
|
58
59
|
readonly deadLettered: boolean;
|
|
59
60
|
readonly processed: number;
|
|
60
61
|
readonly failed: number;
|
|
62
|
+
readonly failedEventId: bigint | null;
|
|
61
63
|
readonly resolvedPendingIds: readonly bigint[];
|
|
62
64
|
};
|
|
63
65
|
export declare function deliverEvents(consumer: EventConsumer, events: ReadonlyArray<StoredEventRow>, context: AppContext, maxAttempts: number, state: ConsumerStateRow): Promise<DeliveryOutcome>;
|