@cosmicdrift/kumiko-framework 0.342.0 → 0.344.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/api/auth-routes.d.ts +2 -0
- package/dist/api/auth-routes.js +35 -2
- package/dist/api/request-context.d.ts +2 -0
- package/dist/api/request-id-middleware.d.ts +2 -0
- package/dist/api/request-id-middleware.js +4 -1
- package/dist/api/routes.js +5 -5
- package/dist/api/server.d.ts +3 -0
- package/dist/api/server.js +4 -1
- package/dist/compliance/duration-spec.d.ts +2 -1
- package/dist/compliance/duration-spec.js +11 -0
- package/dist/compliance/index.d.ts +2 -2
- package/dist/compliance/index.js +1 -1
- package/dist/compliance/profiles.d.ts +6 -5
- package/dist/db/entity-field-encryption.d.ts +1 -1
- package/dist/db/entity-field-encryption.js +5 -5
- package/dist/db/event-store-executor-write.js +38 -11
- package/dist/db/projection-registry-binding.d.ts +3 -0
- package/dist/db/projection-registry-binding.js +12 -0
- package/dist/db/tenant-db.d.ts +2 -0
- package/dist/db/tenant-db.js +22 -7
- package/dist/engine/boot-validator/index.d.ts +11 -0
- package/dist/engine/boot-validator/index.js +45 -1
- package/dist/engine/boot-validator/query-output-columns.js +8 -7
- package/dist/engine/boot-validator/query-refs.js +3 -0
- package/dist/engine/boot-validator/screens.js +28 -0
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +1 -1
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/types/index.js +1 -1
- package/dist/env/index.d.ts +3 -0
- package/dist/env/index.js +12 -0
- package/dist/errors/docs-url.d.ts +7 -0
- package/dist/errors/docs-url.js +36 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.js +1 -0
- package/dist/errors/kumiko-error.d.ts +0 -2
- package/dist/errors/kumiko-error.js +0 -24
- package/dist/errors/serialize.d.ts +3 -2
- package/dist/errors/serialize.js +4 -2
- package/dist/jobs/job-runner.js +31 -2
- package/dist/pipeline/dispatch-shared.js +3 -1
- package/dist/pipeline/event-retention.d.ts +1 -0
- package/dist/pipeline/event-retention.js +6 -0
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/projections-runner.d.ts +1 -0
- package/dist/pipeline/projections-runner.js +23 -14
- package/dist/rate-limit/middleware.js +1 -1
- package/dist/stack/test-stack.d.ts +7 -0
- package/dist/stack/test-stack.js +6 -0
- package/package.json +4 -4
- package/src/changes.json +32 -0
|
@@ -34,6 +34,50 @@ export { validateAppCustomScreenWriteQns } from "./custom-screen-write-qns.js";
|
|
|
34
34
|
export { collectWriteHandlerQns } from "./nav.js";
|
|
35
35
|
export { SECURITY_BASELINE_FEATURE_NAMES } from "./security-baseline.js";
|
|
36
36
|
export { MAX_TRANSFER_DEPTH } from "./transfer-graph.js";
|
|
37
|
+
// Test stacks mount feature subsets, so a reference into a feature that is not
|
|
38
|
+
// mounted cannot be judged: it counts as present. A reference into a mounted
|
|
39
|
+
// feature must still resolve.
|
|
40
|
+
function mountedPrefixOf(qn) {
|
|
41
|
+
return qn.split(":", 1)[0] ?? qn;
|
|
42
|
+
}
|
|
43
|
+
class PrefixTolerantSet extends Set {
|
|
44
|
+
mountedFeatures;
|
|
45
|
+
constructor(members, mountedFeatures) {
|
|
46
|
+
super(members);
|
|
47
|
+
this.mountedFeatures = mountedFeatures;
|
|
48
|
+
}
|
|
49
|
+
has(qn) {
|
|
50
|
+
return super.has(qn) || !this.mountedFeatures.has(mountedPrefixOf(qn));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
class PrefixTolerantMap extends Map {
|
|
54
|
+
mountedFeatures;
|
|
55
|
+
constructor(members, mountedFeatures) {
|
|
56
|
+
super(members);
|
|
57
|
+
this.mountedFeatures = mountedFeatures;
|
|
58
|
+
}
|
|
59
|
+
has(qn) {
|
|
60
|
+
return super.has(qn) || !this.mountedFeatures.has(mountedPrefixOf(qn));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Subset-safe slice of validateBoot for test stacks: nav references (screen,
|
|
65
|
+
* parent, workspace, tree actions), workspaces and nav cycles. The checks that
|
|
66
|
+
* need the whole app composition (screens, ref entities) run only in validateBoot.
|
|
67
|
+
*/
|
|
68
|
+
export function validateNavBootSubset(rawFeatures) {
|
|
69
|
+
const features = dedupeFeatures(rawFeatures);
|
|
70
|
+
const mounted = new Set(features.map((f) => f.name));
|
|
71
|
+
const allNavQns = collectNavQns(features);
|
|
72
|
+
const tolerantScreens = new PrefixTolerantSet(collectScreenQns(features), mounted);
|
|
73
|
+
const tolerantNavs = new PrefixTolerantMap(allNavQns, mounted);
|
|
74
|
+
const tolerantWorkspaces = new PrefixTolerantMap(collectWorkspaceQns(features), mounted);
|
|
75
|
+
for (const feature of features) {
|
|
76
|
+
validateNavs(feature, tolerantScreens, tolerantNavs, tolerantWorkspaces);
|
|
77
|
+
validateWorkspaces(feature, tolerantNavs);
|
|
78
|
+
}
|
|
79
|
+
validateNavCycles(allNavQns);
|
|
80
|
+
}
|
|
37
81
|
/**
|
|
38
82
|
* Validates all feature configurations at boot time.
|
|
39
83
|
* Throws on the first error found — fail fast.
|
|
@@ -187,7 +231,7 @@ export function validateBoot(rawFeatures, options) {
|
|
|
187
231
|
// catches malformed keys (wrong length, bad base64) at boot instead of
|
|
188
232
|
// on the first encrypted read in prod. An injected cipher (test seam,
|
|
189
233
|
// custom KMS provider) satisfies the requirement without env keys.
|
|
190
|
-
validateEntityFieldEncryptionAvailable(options?.env);
|
|
234
|
+
validateEntityFieldEncryptionAvailable(options?.env, options?.entityFieldCipher);
|
|
191
235
|
}
|
|
192
236
|
if (hasFileFields && !(options?.env ?? process.env)[FILE_STORAGE_PROVIDER_ENV]) {
|
|
193
237
|
throw new Error(`${FILE_STORAGE_PROVIDER_ENV} environment variable is required (file/image fields in use)`);
|
|
@@ -165,13 +165,13 @@ function checkDashboardStatPanelFields(queryHandlers, featureName, screenId, pan
|
|
|
165
165
|
checkPanelField("deltaToneField", panel.deltaToneField);
|
|
166
166
|
}
|
|
167
167
|
// visibleWhen reads a flat record like stat panels; a typo'd field would hide
|
|
168
|
-
// the panel forever without any message.
|
|
169
|
-
function
|
|
170
|
-
// skip:
|
|
171
|
-
if (
|
|
168
|
+
// the panel or screen forever without any message.
|
|
169
|
+
function checkVisibleWhenField(queryHandlers, visibleWhen, owner) {
|
|
170
|
+
// skip: always visible, no field to check
|
|
171
|
+
if (visibleWhen === undefined)
|
|
172
172
|
return;
|
|
173
|
-
const { query, field } =
|
|
174
|
-
checkFieldExists(getZodObjectShape(queryHandlers.get(query)?.outputSchema), field, () =>
|
|
173
|
+
const { query, field } = visibleWhen;
|
|
174
|
+
checkFieldExists(getZodObjectShape(queryHandlers.get(query)?.outputSchema), field, () => `${owner} visibleWhen references field "${field}" which is not present in query "${query}"'s outputSchema.`);
|
|
175
175
|
}
|
|
176
176
|
function checkDashboardOutputFields(queryHandlers, featureName, screenId, screen) {
|
|
177
177
|
for (const panel of screen.panels) {
|
|
@@ -184,7 +184,7 @@ function checkDashboardOutputFields(queryHandlers, featureName, screenId, screen
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
else if (panel.kind === "screen") {
|
|
187
|
-
|
|
187
|
+
checkVisibleWhenField(queryHandlers, panel.visibleWhen, `[Feature ${featureName}] Screen "${screenId}" (dashboard) screen-panel "${panel.id}"`);
|
|
188
188
|
}
|
|
189
189
|
else if (panel.kind === "list") {
|
|
190
190
|
const rowShape = resolveListRowShape(queryHandlers, panel.query, `[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}"`);
|
|
@@ -197,6 +197,7 @@ function checkDashboardOutputFields(queryHandlers, featureName, screenId, screen
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
function checkScreenOutputColumns(queryHandlers, featureName, screenId, screen) {
|
|
200
|
+
checkVisibleWhenField(queryHandlers, screen.visibleWhen, `[Feature ${featureName}] Screen "${screenId}" (${screen.type})`);
|
|
200
201
|
if (screen.type === "projectionList") {
|
|
201
202
|
checkProjectionListOutputColumns(queryHandlers, featureName, screenId, screen);
|
|
202
203
|
}
|
|
@@ -52,6 +52,9 @@ function checkFormFieldQueryRefs(queryHandlers, featureName, screenId, screenTyp
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
function checkScreenQueryRefs(queryHandlers, featureName, screenId, screen) {
|
|
55
|
+
if (screen.visibleWhen !== undefined) {
|
|
56
|
+
checkQueryRef(queryHandlers, screen.visibleWhen.query, () => `[Feature ${featureName}] Screen "${screenId}" (${screen.type}) visibleWhen`);
|
|
57
|
+
}
|
|
55
58
|
if (screen.type === "projectionList") {
|
|
56
59
|
checkQueryRef(queryHandlers, screen.query, () => `[Feature ${featureName}] Screen "${screenId}" (projectionList)`);
|
|
57
60
|
}
|
|
@@ -636,6 +636,33 @@ function validateRecordActionRedirect(feature, screenId, screenKind, action, all
|
|
|
636
636
|
validateRedirectTarget(feature, screenId, screenKind, action.redirect, allScreenQns);
|
|
637
637
|
}
|
|
638
638
|
}
|
|
639
|
+
function validateScreenVisibleWhen(feature, screenId, screen, allScreenQns, featureMap) {
|
|
640
|
+
const { visibleWhen, fallback } = screen;
|
|
641
|
+
const context = `[Feature ${feature.name}] Screen "${screenId}" (${screen.type})`;
|
|
642
|
+
if (visibleWhen === undefined && fallback !== undefined) {
|
|
643
|
+
throw new Error(`${context} declares fallback without visibleWhen.`);
|
|
644
|
+
}
|
|
645
|
+
if (visibleWhen !== undefined && (visibleWhen.query === "" || visibleWhen.field === "")) {
|
|
646
|
+
throw new Error(`${context} visibleWhen needs a non-empty query and field.`);
|
|
647
|
+
}
|
|
648
|
+
if (fallback !== undefined) {
|
|
649
|
+
validateScreenFallback(feature, screenId, screen.type, fallback, allScreenQns, featureMap);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function validateScreenFallback(feature, screenId, screenType, fallback, allScreenQns, featureMap) {
|
|
653
|
+
const context = `[Feature ${feature.name}] Screen "${screenId}" (${screenType})`;
|
|
654
|
+
validateScreenNavTarget(feature.name, screenId, screenType, "fallback", fallback, allScreenQns, feature.screens);
|
|
655
|
+
const fallbackQn = resolveScreenTargetQn(feature.name, fallback);
|
|
656
|
+
if (fallbackQn === qualifyEntityName(feature.name, "screen", screenId)) {
|
|
657
|
+
throw new Error(`${context} fallback points at the screen itself.`);
|
|
658
|
+
}
|
|
659
|
+
// A gated fallback could hide in turn and chain or cycle (A→B→A); one hop keeps the
|
|
660
|
+
// runtime gate trivially terminating.
|
|
661
|
+
const [fallbackFeatureName = "", , fallbackShortId = ""] = fallbackQn.split(":");
|
|
662
|
+
if (featureMap.get(fallbackFeatureName)?.screens[fallbackShortId]?.visibleWhen !== undefined) {
|
|
663
|
+
throw new Error(`${context} fallback "${fallback}" has its own visibleWhen; a fallback must always render.`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
639
666
|
function validateInlineFormNavTargets(feature, screenId, screen, kind, allScreenQns) {
|
|
640
667
|
if (screen.redirect !== undefined) {
|
|
641
668
|
validateRedirectTarget(feature, screenId, kind, screen.redirect, allScreenQns);
|
|
@@ -869,6 +896,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
869
896
|
};
|
|
870
897
|
for (const [screenId, screen] of Object.entries(feature.screens)) {
|
|
871
898
|
validateScreenHasNavArea(feature, screenId, screen, featureMap);
|
|
899
|
+
validateScreenVisibleWhen(feature, screenId, screen, allScreenQns, featureMap);
|
|
872
900
|
if (screen.type === "custom") {
|
|
873
901
|
if (!screen.renderer.react && !screen.renderer.native) {
|
|
874
902
|
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" has type="custom" but the renderer ` +
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ export { WORKFLOW_SYSTEM_EVENT_DEFS } from "./steps/workflow-system-events.js";
|
|
|
74
74
|
export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
|
|
75
75
|
export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
|
|
76
76
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
|
|
77
|
-
export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DateFieldDef, DecimalFieldDef, DeleteContext, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EntityDefinition, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
|
|
78
|
-
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, HookPhases, isAgentVisibleScreen, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
|
|
77
|
+
export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, 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, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
|
|
78
|
+
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
|
|
79
79
|
export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.js";
|
|
80
80
|
export { runValidation } from "./validation.js";
|
package/dist/engine/index.js
CHANGED
|
@@ -68,5 +68,5 @@ export { WORKFLOW_SYSTEM_EVENT_DEFS } from "./steps/workflow-system-events.js";
|
|
|
68
68
|
export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
|
|
69
69
|
export { findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, } from "./tier-resolver-extension.js";
|
|
70
70
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
|
|
71
|
-
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, HookPhases, isAgentVisibleScreen, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
|
|
71
|
+
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
|
|
72
72
|
export { runValidation } from "./validation.js";
|
|
@@ -7,8 +7,8 @@ export type { ExtensionOptionsArgs, ExtensionOptionsFor, KumikoExtensionOptionsM
|
|
|
7
7
|
export type { BootCheckContext, BootCheckFn, FeatureDefinition, FeatureMetricDef, FeatureMetricType, FeatureRegistrar, MetricOptions, Registry, SecretKeyDefinition, SecretKeyHandle, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretOptions, StoreTableDef, StoreTableEntry, StoreTableOptions, UiHintOption, UiHints, } from "@cosmicdrift/kumiko-types/feature";
|
|
8
8
|
export type { FieldIconKey } from "@cosmicdrift/kumiko-types/field-icon";
|
|
9
9
|
export type { AnyFileFieldDef, BigIntFieldDef, BooleanFieldDef, ClientDerivedFieldDef, DateFieldDef, DecimalFieldDef, DefaultCurrency, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EmbeddedDerivedCellDef, EmbeddedFieldDef, EmbeddedSubFieldDef, EntityDefinition, EntityIndexDef, FieldAccess, FieldDefinition, FieldsMap, FileFieldDef, FilesFieldDef, Findability, ImageFieldDef, ImagesFieldDef, JsonbFieldDef, LocatedTimestampFieldDef, LongTextFieldDef, LongTextFindability, MoneyCurrencySource, MoneyFieldDef, MultiSelectFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, ReferenceFieldDef, ResolvedPiiFlags, RetentionDef, SelectFieldDef, SelectOptionTone, TextFieldDef, TimestampFieldDef, TransitionMap, TzFieldDef, } from "@cosmicdrift/kumiko-types/fields";
|
|
10
|
-
export type { AccessRule, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AggregateStreamHandle, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, CamelToKebab, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadRateLimitOption, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
|
|
11
|
-
export { isOpenToAllGranted, isRateLimitDisabled, isUiAccessGranted, } from "@cosmicdrift/kumiko-types/handlers";
|
|
10
|
+
export type { AccessRule, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AggregateStreamHandle, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadRateLimitOption, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
|
|
11
|
+
export { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, isOpenToAllGranted, isRateLimitDisabled, isUiAccessGranted, } from "@cosmicdrift/kumiko-types/handlers";
|
|
12
12
|
export type { DeleteContext, EntityHookMap, HookMap, HookOwner, HookPhase, LifecycleHookFn, LifecycleResult, OwnedFn, PhasedHook, PostDeleteBatchHookFn, PostDeleteHookFn, PostQueryHookFn, PostSaveBatchHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, SaveContext, SearchPayloadContributorFn, ValidationError, ValidationHookFn, } from "@cosmicdrift/kumiko-types/hooks";
|
|
13
13
|
export type { HttpRouteDefinition, HttpRouteHandler, HttpRouteHandlerDeps, HttpRouteMethod, } from "@cosmicdrift/kumiko-types/http-route";
|
|
14
14
|
export type { EntityId, TenantId } from "@cosmicdrift/kumiko-types/identifiers";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Barrel: re-exports all types from @cosmicdrift/kumiko-types, plus the
|
|
2
2
|
// runtime helpers below that stay framework-side.
|
|
3
3
|
export { isAgentVisibleScreen, resolveAgentExposure, } from "@cosmicdrift/kumiko-types/agent-exposure";
|
|
4
|
-
export { isOpenToAllGranted, isRateLimitDisabled, isUiAccessGranted, } from "@cosmicdrift/kumiko-types/handlers";
|
|
4
|
+
export { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, isOpenToAllGranted, isRateLimitDisabled, isUiAccessGranted, } from "@cosmicdrift/kumiko-types/handlers";
|
|
5
5
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-types/identifiers";
|
|
6
6
|
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
7
7
|
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, isFileField } from "../field-helpers.js";
|
package/dist/env/index.d.ts
CHANGED
|
@@ -57,6 +57,9 @@ export type ComposedEnvSchema = {
|
|
|
57
57
|
* "app" from `extend`, or "framework-core" from `core`. */
|
|
58
58
|
readonly sources: Readonly<Record<string, string>>;
|
|
59
59
|
};
|
|
60
|
+
/** Makes the named env keys optional while keeping their `kms` meta. Keys
|
|
61
|
+
* missing from the schema shape are ignored. */
|
|
62
|
+
export declare function withOptionalEnvKeys(schema: z.ZodObject<z.ZodRawShape>, keys: readonly string[]): z.ZodObject<z.ZodRawShape>;
|
|
60
63
|
export declare function composeEnvSchema(options: ComposeEnvSchemaOptions): ComposedEnvSchema;
|
|
61
64
|
export type EnvErrorKind = "missing" | "invalid";
|
|
62
65
|
export type EnvError = {
|
package/dist/env/index.js
CHANGED
|
@@ -123,6 +123,18 @@ function optionalKeepingKmsMeta(field) {
|
|
|
123
123
|
? optional.meta({ kumiko: readKumikoMeta(field) })
|
|
124
124
|
: optional;
|
|
125
125
|
}
|
|
126
|
+
/** Makes the named env keys optional while keeping their `kms` meta. Keys
|
|
127
|
+
* missing from the schema shape are ignored. */
|
|
128
|
+
export function withOptionalEnvKeys(schema, keys) {
|
|
129
|
+
const shape = zodShape(schema);
|
|
130
|
+
const relaxed = {};
|
|
131
|
+
for (const key of keys) {
|
|
132
|
+
const field = shape[key];
|
|
133
|
+
if (field)
|
|
134
|
+
relaxed[key] = optionalKeepingKmsMeta(field);
|
|
135
|
+
}
|
|
136
|
+
return Object.keys(relaxed).length === 0 ? schema : schema.safeExtend(relaxed);
|
|
137
|
+
}
|
|
126
138
|
export function composeEnvSchema(options) {
|
|
127
139
|
const optionalSet = new Set(options.optionalFeatures ?? []);
|
|
128
140
|
const merged = {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { KumikoError } from "./kumiko-error.js";
|
|
2
|
+
/** Links an app's own error reasons to its own docs; without it only framework reasons carry a docsUrl. */
|
|
3
|
+
export type ErrorDocsConfig = {
|
|
4
|
+
readonly baseUrl: string;
|
|
5
|
+
readonly reasons?: readonly string[] | "all";
|
|
6
|
+
};
|
|
7
|
+
export declare function resolveErrorDocsUrl(err: KumikoError, errorDocs?: ErrorDocsConfig): string | undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AgentReasons, FrameworkReasons } from "./reasons.js";
|
|
2
|
+
const DEFAULT_DOCS_BASE_URL = "https://docs.kumiko.rocks";
|
|
3
|
+
const REASON_SLUG_RE = /^[a-z0-9_.-]+$/;
|
|
4
|
+
const frameworkReasons = new Set([
|
|
5
|
+
...Object.values(FrameworkReasons),
|
|
6
|
+
...Object.values(AgentReasons),
|
|
7
|
+
]);
|
|
8
|
+
function reasonOf(err) {
|
|
9
|
+
if (!err.details || typeof err.details !== "object")
|
|
10
|
+
return undefined;
|
|
11
|
+
// @cast-boundary error-details — per-error typed details; reflection shape only for the reason lookup.
|
|
12
|
+
const reason = err.details["reason"];
|
|
13
|
+
return typeof reason === "string" && REASON_SLUG_RE.test(reason) ? reason : undefined;
|
|
14
|
+
}
|
|
15
|
+
function isCoveredByConfig(reason, errorDocs) {
|
|
16
|
+
if (!errorDocs)
|
|
17
|
+
return false;
|
|
18
|
+
return errorDocs.reasons === "all" || (errorDocs.reasons?.includes(reason) ?? false);
|
|
19
|
+
}
|
|
20
|
+
function errorsUrl(baseUrl, slug) {
|
|
21
|
+
return `${baseUrl}/errors/${encodeURIComponent(slug)}`;
|
|
22
|
+
}
|
|
23
|
+
// A reason the framework documents (or the error code itself) links to the framework docs,
|
|
24
|
+
// overridable via KUMIKO_DOCS_URL. An app's own reason links only where `errorDocs` covers it —
|
|
25
|
+
// otherwise the framework docs would have no page for it.
|
|
26
|
+
export function resolveErrorDocsUrl(err, errorDocs) {
|
|
27
|
+
const reason = reasonOf(err);
|
|
28
|
+
if (reason === undefined || reason === err.code || frameworkReasons.has(reason)) {
|
|
29
|
+
const baseUrl = process.env["KUMIKO_DOCS_URL"] ?? DEFAULT_DOCS_BASE_URL;
|
|
30
|
+
return errorsUrl(baseUrl, reason ?? err.code);
|
|
31
|
+
}
|
|
32
|
+
if (errorDocs && isCoveredByConfig(reason, errorDocs)) {
|
|
33
|
+
return errorsUrl(errorDocs.baseUrl, reason);
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type { FeatureDisabledDetails, FieldIssue, NotFoundDetails, PreconditionFailedDetails, RateLimitDetails, UnconfiguredDetails, UniqueViolationDetails, UnprocessableOpts, ValidationDetails, VersionConflictDetails, } from "./classes.js";
|
|
2
2
|
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
3
|
+
export type { ErrorDocsConfig } from "./docs-url.js";
|
|
4
|
+
export { resolveErrorDocsUrl } from "./docs-url.js";
|
|
3
5
|
export type { ErrorCtorInput, ErrorOpts } from "./kumiko-error.js";
|
|
4
6
|
export { isKumikoError, KumikoError } from "./kumiko-error.js";
|
|
5
7
|
export { memberResolutionReadOnlyDenied } from "./member-resolution.js";
|
package/dist/errors/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
2
|
+
export { resolveErrorDocsUrl } from "./docs-url.js";
|
|
2
3
|
export { isKumikoError, KumikoError } from "./kumiko-error.js";
|
|
3
4
|
export { memberResolutionReadOnlyDenied } from "./member-resolution.js";
|
|
4
5
|
export { AgentReasons, FrameworkReasons } from "./reasons.js";
|
|
@@ -19,7 +19,5 @@ export declare abstract class KumikoError extends Error {
|
|
|
19
19
|
readonly i18nParams: Readonly<Record<string, unknown>> | undefined;
|
|
20
20
|
readonly details: unknown;
|
|
21
21
|
constructor(input: ErrorCtorInput);
|
|
22
|
-
get docsUrl(): string;
|
|
23
|
-
private get reasonSlug();
|
|
24
22
|
}
|
|
25
23
|
export declare function isKumikoError(e: unknown): e is KumikoError;
|
|
@@ -2,13 +2,6 @@
|
|
|
2
2
|
// Anything that isn't a KumikoError is treated as an unexpected crash and
|
|
3
3
|
// auto-wrapped into InternalError by the dispatcher — so the HTTP layer never
|
|
4
4
|
// has to guess the status or the client-facing shape.
|
|
5
|
-
// Default docs URL for self-service errors. Override via `KUMIKO_DOCS_URL`
|
|
6
|
-
// (e.g. self-hosted customers pointing at their own docs instance).
|
|
7
|
-
const DEFAULT_DOCS_BASE_URL = "https://docs.kumiko.rocks";
|
|
8
|
-
const REASON_SLUG_RE = /^[a-z0-9_.-]+$/;
|
|
9
|
-
function docsBaseUrl() {
|
|
10
|
-
return process.env["KUMIKO_DOCS_URL"] ?? DEFAULT_DOCS_BASE_URL;
|
|
11
|
-
}
|
|
12
5
|
export class KumikoError extends Error {
|
|
13
6
|
i18nKey;
|
|
14
7
|
i18nParams;
|
|
@@ -20,23 +13,6 @@ export class KumikoError extends Error {
|
|
|
20
13
|
this.i18nParams = input.i18nParams;
|
|
21
14
|
this.details = input.details;
|
|
22
15
|
}
|
|
23
|
-
// Docs URL for self-service. Prefer a well-formed `details.reason` slug
|
|
24
|
-
// (e.g. ConflictError → "stale_state"); otherwise fall back to the error
|
|
25
|
-
// code. Encode the path segment so spaces/`../` in free-form reasons
|
|
26
|
-
// cannot break or redirect the link the default renderer shows.
|
|
27
|
-
get docsUrl() {
|
|
28
|
-
return `${docsBaseUrl()}/errors/${encodeURIComponent(this.reasonSlug)}`;
|
|
29
|
-
}
|
|
30
|
-
get reasonSlug() {
|
|
31
|
-
if (this.details && typeof this.details === "object") {
|
|
32
|
-
// @cast-boundary error-details — per-error typed details; reflection
|
|
33
|
-
// shape only for the reasonSlug lookup.
|
|
34
|
-
const r = this.details["reason"];
|
|
35
|
-
if (typeof r === "string" && REASON_SLUG_RE.test(r))
|
|
36
|
-
return r;
|
|
37
|
-
}
|
|
38
|
-
return this.code;
|
|
39
|
-
}
|
|
40
16
|
}
|
|
41
17
|
export function isKumikoError(e) {
|
|
42
18
|
return e instanceof KumikoError;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ErrorDocsConfig } from "./docs-url.js";
|
|
1
2
|
import type { KumikoError } from "./kumiko-error.js";
|
|
2
3
|
export type ErrorResponseBody = {
|
|
3
4
|
readonly error: {
|
|
@@ -6,12 +7,12 @@ export type ErrorResponseBody = {
|
|
|
6
7
|
readonly i18nParams?: Readonly<Record<string, unknown>>;
|
|
7
8
|
readonly message: string;
|
|
8
9
|
readonly details?: unknown;
|
|
9
|
-
readonly docsUrl
|
|
10
|
+
readonly docsUrl?: string;
|
|
10
11
|
readonly requestId?: string;
|
|
11
12
|
readonly timestamp: string;
|
|
12
13
|
};
|
|
13
14
|
};
|
|
14
|
-
export declare function serializeError(err: KumikoError, requestId?: string): ErrorResponseBody;
|
|
15
|
+
export declare function serializeError(err: KumikoError, requestId?: string, errorDocs?: ErrorDocsConfig): ErrorResponseBody;
|
|
15
16
|
export type ErrorLogEntry = {
|
|
16
17
|
readonly name: string;
|
|
17
18
|
readonly code: string;
|
package/dist/errors/serialize.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveErrorDocsUrl } from "./docs-url.js";
|
|
1
2
|
// InternalError deliberately omits `details` from the response in production,
|
|
2
3
|
// because that field often carries the internal cause. The log still has
|
|
3
4
|
// everything (see buildErrorLog).
|
|
@@ -19,7 +20,7 @@ function devCauseDetail(cause) {
|
|
|
19
20
|
...(cause.stack && { causeStack: cause.stack.split("\n").slice(0, 8).join("\n") }),
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
|
-
export function serializeError(err, requestId) {
|
|
23
|
+
export function serializeError(err, requestId, errorDocs) {
|
|
23
24
|
const stripped = CODES_WITHOUT_CLIENT_DETAILS.has(err.code) && isProductionEnv();
|
|
24
25
|
const exposeDeclaredDetails = err.details !== undefined && !stripped;
|
|
25
26
|
// For internal_error in dev/test, surface the cause chain so the
|
|
@@ -34,6 +35,7 @@ export function serializeError(err, requestId) {
|
|
|
34
35
|
}
|
|
35
36
|
return undefined;
|
|
36
37
|
})();
|
|
38
|
+
const docsUrl = resolveErrorDocsUrl(err, errorDocs);
|
|
37
39
|
return {
|
|
38
40
|
error: {
|
|
39
41
|
code: err.code,
|
|
@@ -41,7 +43,7 @@ export function serializeError(err, requestId) {
|
|
|
41
43
|
...(err.i18nParams && { i18nParams: err.i18nParams }),
|
|
42
44
|
message: err.message,
|
|
43
45
|
...(detailsForResponse !== undefined && { details: detailsForResponse }),
|
|
44
|
-
docsUrl
|
|
46
|
+
...(docsUrl !== undefined && { docsUrl }),
|
|
45
47
|
...(requestId && { requestId }),
|
|
46
48
|
timestamp: new Date().toISOString(),
|
|
47
49
|
},
|
package/dist/jobs/job-runner.js
CHANGED
|
@@ -159,6 +159,22 @@ function missingDbProxy(jobName) {
|
|
|
159
159
|
},
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
|
+
function createCrossTenantReads(systemModeDb, reason, report) {
|
|
163
|
+
return {
|
|
164
|
+
selectMany(table, where, options) {
|
|
165
|
+
report("cross-tenant-read", reason);
|
|
166
|
+
return systemModeDb.selectMany(table, where, options);
|
|
167
|
+
},
|
|
168
|
+
fetchOne(table, where) {
|
|
169
|
+
report("cross-tenant-read", reason);
|
|
170
|
+
return systemModeDb.fetchOne(table, where);
|
|
171
|
+
},
|
|
172
|
+
count(table, where) {
|
|
173
|
+
report("cross-tenant-read", reason);
|
|
174
|
+
return systemModeDb.count(table, where);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
162
178
|
function priorAttemptsOf(data) {
|
|
163
179
|
const value = data["_priorAttempts"];
|
|
164
180
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : 0;
|
|
@@ -317,7 +333,11 @@ export function createJobRunner(options) {
|
|
|
317
333
|
if (context.meter)
|
|
318
334
|
registerStandardMetrics(context.meter);
|
|
319
335
|
// Instance-scoped, not module-level: each runner (and each test) audits a system cron's declared hatch once.
|
|
320
|
-
const systemCronEscapeHatchDedup = createEscapeHatchProcessDedup([
|
|
336
|
+
const systemCronEscapeHatchDedup = createEscapeHatchProcessDedup([
|
|
337
|
+
"unsafe-raw",
|
|
338
|
+
"global-write",
|
|
339
|
+
"cross-tenant-read",
|
|
340
|
+
]);
|
|
321
341
|
// Set at the top of stop() — a graceful shutdown closes the redis/BullMQ
|
|
322
342
|
// clients itself, which fires the exact same 'error' listeners below with
|
|
323
343
|
// an expected "Connection is closed." Downgrading to debug once stopping
|
|
@@ -690,7 +710,10 @@ export function createJobRunner(options) {
|
|
|
690
710
|
// unchecked cross-tenant escape hatch for such a job. Gated like jobDb so
|
|
691
711
|
// ctx.systemDb, built from it, is gated too.
|
|
692
712
|
const tenantScopedDb = configDb
|
|
693
|
-
? createTenantDb(configDb, tenantId, "system", undefined, undefined, undefined,
|
|
713
|
+
? createTenantDb(configDb, tenantId, "system", undefined, undefined, undefined, {
|
|
714
|
+
projectionRegistry: registry,
|
|
715
|
+
...(jobPersonalDataGate && { personalDataGate: jobPersonalDataGate }),
|
|
716
|
+
})
|
|
694
717
|
: undefined;
|
|
695
718
|
const isSystemJob = registry.isJobSystemScoped(jobName);
|
|
696
719
|
// One reporter for ctx.systemDb and ctx.db.unsafeRaw() so both dedupe in the same window.
|
|
@@ -712,9 +735,14 @@ export function createJobRunner(options) {
|
|
|
712
735
|
unsafeRaw: escapeHatchFor(jobDef.escapeHatch, "unsafeRaw"),
|
|
713
736
|
globalWrites: escapeHatchFor(jobDef.escapeHatch, "globalWrites"),
|
|
714
737
|
report: reportEscapeHatch,
|
|
738
|
+
projectionRegistry: registry,
|
|
715
739
|
...(jobPersonalDataGate && { personalDataGate: jobPersonalDataGate }),
|
|
716
740
|
})
|
|
717
741
|
: undefined;
|
|
742
|
+
const crossTenantReadsReason = escapeHatchFor(jobDef.escapeHatch, "unsafeRaw")?.reason;
|
|
743
|
+
const crossTenantReads = tenantScopedDb && crossTenantReadsReason !== undefined
|
|
744
|
+
? createCrossTenantReads(tenantScopedDb, crossTenantReadsReason, reportEscapeHatch)
|
|
745
|
+
: undefined;
|
|
718
746
|
const config = context._configAccessorFactory && tenantScopedDb
|
|
719
747
|
? context._configAccessorFactory({
|
|
720
748
|
user: { id: jobSystemUser.id, tenantId },
|
|
@@ -743,6 +771,7 @@ export function createJobRunner(options) {
|
|
|
743
771
|
...(notify !== undefined && { notify }),
|
|
744
772
|
...(config !== undefined && { config }),
|
|
745
773
|
...(systemDb && { systemDb }),
|
|
774
|
+
...(crossTenantReads && { crossTenantReads }),
|
|
746
775
|
// The runner owns the registry it resolved this job from — expose it so
|
|
747
776
|
// workers can reach projections/jobs without the app author duplicating
|
|
748
777
|
// it into `context` (the JobContext contract guarantees `registry`).
|
|
@@ -178,6 +178,7 @@ export async function buildHandlerContext(ctx, type, user, origin, tx, afterComm
|
|
|
178
178
|
report: reportEscapeHatch,
|
|
179
179
|
memberReadOnly: isMemberResolutionPrincipal(user),
|
|
180
180
|
personalDataGate: buildPersonalDataGate(registry, origin),
|
|
181
|
+
projectionRegistry: registry,
|
|
181
182
|
});
|
|
182
183
|
// Propagate the request's AbortSignal so every TenantDb query throws when
|
|
183
184
|
// the client has disconnected — handlers with many sequential queries skip
|
|
@@ -244,7 +245,7 @@ export async function buildHandlerContext(ctx, type, user, origin, tx, afterComm
|
|
|
244
245
|
const targetSystemUser = createSystemUser(targetTenantId);
|
|
245
246
|
return configFactory({
|
|
246
247
|
user: { id: targetSystemUser.id, tenantId: targetTenantId },
|
|
247
|
-
db: createTenantDb(dbSource, targetTenantId, "tenant", context.tracer, context.meter),
|
|
248
|
+
db: createTenantDb(dbSource, targetTenantId, "tenant", context.tracer, context.meter, undefined, { projectionRegistry: registry }),
|
|
248
249
|
secrets: context.secrets,
|
|
249
250
|
});
|
|
250
251
|
}
|
|
@@ -983,6 +984,7 @@ function buildAuthClaimsContext(ctx, user) {
|
|
|
983
984
|
log: context.log,
|
|
984
985
|
window: ctx.escapeHatchReportWindow,
|
|
985
986
|
}),
|
|
987
|
+
projectionRegistry: ctx.registry,
|
|
986
988
|
});
|
|
987
989
|
const configAccessor = context._configAccessorFactory
|
|
988
990
|
? context._configAccessorFactory({
|
|
@@ -4,6 +4,7 @@ export type PruneEventsOptions = {
|
|
|
4
4
|
readonly olderThan?: Temporal.Instant;
|
|
5
5
|
readonly olderThanDays?: number;
|
|
6
6
|
readonly aggregateTypes: readonly string[];
|
|
7
|
+
readonly aggregateIds?: readonly string[];
|
|
7
8
|
readonly dryRun?: boolean;
|
|
8
9
|
};
|
|
9
10
|
export type PruneEventsResult = {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
1
2
|
import { lockEventConsumersShareMode } from "../db/queries/event-consumer.js";
|
|
2
3
|
import { deleteMany, selectMany, transaction } from "../db/query.js";
|
|
3
4
|
import { eventsTable } from "../event-store/index.js";
|
|
@@ -30,7 +31,11 @@ export async function pruneEvents(db, options) {
|
|
|
30
31
|
if (!options.aggregateTypes || options.aggregateTypes.length === 0) {
|
|
31
32
|
throw new Error("pruneEvents: aggregateTypes is required and must be non-empty. Pruning the event log is destructive — name the aggregate types to delete explicitly.");
|
|
32
33
|
}
|
|
34
|
+
if (options.aggregateIds !== undefined && options.aggregateIds.length === 0) {
|
|
35
|
+
throw new Error("pruneEvents: aggregateIds must be non-empty when set.");
|
|
36
|
+
}
|
|
33
37
|
const aggregateTypes = options.aggregateTypes;
|
|
38
|
+
const aggregateIds = options.aggregateIds;
|
|
34
39
|
const dryRun = options.dryRun === true;
|
|
35
40
|
return transaction(db, async (tx) => {
|
|
36
41
|
// Serialise against consumer-bootstrap INSERTs. Without this, the race
|
|
@@ -49,6 +54,7 @@ export async function pruneEvents(db, options) {
|
|
|
49
54
|
// Step 1 — collect candidate event ids.
|
|
50
55
|
const candidates = await selectMany(tx, eventsTable, {
|
|
51
56
|
aggregateType: [...aggregateTypes],
|
|
57
|
+
...(aggregateIds ? { aggregateId: [...aggregateIds] } : {}),
|
|
52
58
|
createdAt: { lt: cutoff },
|
|
53
59
|
});
|
|
54
60
|
if (candidates.length === 0) {
|
package/dist/pipeline/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { resolveAuthClaims } from "./auth-claims-resolver.js";
|
|
|
5
5
|
export { createCascadeDeleteHook } from "./cascade-handler.js";
|
|
6
6
|
export type { Dispatcher } from "./dispatcher.js";
|
|
7
7
|
export { createDispatcher, dispatcherToWriteRef } from "./dispatcher.js";
|
|
8
|
+
export type { FailedWriteResult } from "./dispatcher-utils.js";
|
|
9
|
+
export { isFailedWriteResult } from "./dispatcher-utils.js";
|
|
8
10
|
export type { DistributedLock } from "./distributed-lock.js";
|
|
9
11
|
export { createDistributedLock } from "./distributed-lock.js";
|
|
10
12
|
export type { EntityCache, EntityCacheOptions } from "./entity-cache.js";
|
package/dist/pipeline/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { createEscapeHatchReporter, createEscapeHatchReportWindow, ESCAPE_HATCH_
|
|
|
2
2
|
export { resolveAuthClaims } from "./auth-claims-resolver.js";
|
|
3
3
|
export { createCascadeDeleteHook } from "./cascade-handler.js";
|
|
4
4
|
export { createDispatcher, dispatcherToWriteRef } from "./dispatcher.js";
|
|
5
|
+
export { isFailedWriteResult } from "./dispatcher-utils.js";
|
|
5
6
|
export { createDistributedLock } from "./distributed-lock.js";
|
|
6
7
|
export { createEntityCache } from "./entity-cache.js";
|
|
7
8
|
export { ConsumerStatuses, createEventConsumerStateTable, eventConsumerStateTable, SHARED_INSTANCE_SENTINEL, } from "./event-consumer-state.js";
|
|
@@ -2,4 +2,5 @@ import type { DbRunner } from "../db/index.js";
|
|
|
2
2
|
import type { LifecycleResult, Registry } from "../engine/types/index.js";
|
|
3
3
|
import type { StoredEvent } from "../event-store/index.js";
|
|
4
4
|
export declare function runProjections(result: LifecycleResult, registry: Registry, runner: DbRunner | undefined): Promise<void>;
|
|
5
|
+
export declare function hasLiveProjectionsForEvent(event: StoredEvent, registry: Registry): boolean;
|
|
5
6
|
export declare function runProjectionsForEvent(event: StoredEvent, registry: Registry, tx: DbRunner): Promise<void>;
|
|
@@ -36,22 +36,31 @@ export async function runProjections(result, registry, runner) {
|
|
|
36
36
|
// CRUD path (via runProjections) and the ctx.appendEvent path (domain events
|
|
37
37
|
// emitted inside a write handler). Keeping one function means an auto-event
|
|
38
38
|
// and a r.defineEvent-event land in the same inline-projection pipeline.
|
|
39
|
+
// Events whose projections already ran. The EventStoreExecutor projects right
|
|
40
|
+
// after its write and hands the same object back on the LifecycleResult, so the
|
|
41
|
+
// dispatcher's later runProjections() and manual calls become no-ops instead of
|
|
42
|
+
// double-applying. Marked only after a successful run: a rolled-back write is
|
|
43
|
+
// retried with a fresh event object, so a thrown projection never blocks a retry.
|
|
44
|
+
const projectedEvents = new WeakSet();
|
|
45
|
+
// Implicit projections exist only for rebuildProjection: the EventStoreExecutor
|
|
46
|
+
// already writes their table on the live path, so applying them live would write
|
|
47
|
+
// twice and hit a unique key violation.
|
|
48
|
+
function liveProjectionsForEvent(event, registry) {
|
|
49
|
+
return registry
|
|
50
|
+
.getProjectionsForSource(event.aggregateType)
|
|
51
|
+
.filter((proj) => !proj.isImplicit && proj.apply[event.type] !== undefined);
|
|
52
|
+
}
|
|
53
|
+
export function hasLiveProjectionsForEvent(event, registry) {
|
|
54
|
+
return liveProjectionsForEvent(event, registry).length > 0;
|
|
55
|
+
}
|
|
39
56
|
export async function runProjectionsForEvent(event, registry, tx) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (projections.length === 0)
|
|
57
|
+
// skip: this event object was already projected in this write
|
|
58
|
+
if (projectedEvents.has(event))
|
|
43
59
|
return;
|
|
44
|
-
for (const proj of
|
|
45
|
-
// ImplicitProjections existieren nur für rebuildProjection — der
|
|
46
|
-
// EventStoreExecutor schreibt im Live-Pfad bereits direkt in die
|
|
47
|
-
// Tabelle. Live-Apply der Implicit würde doppelt schreiben → unique
|
|
48
|
-
// key violation. Filter ist Pflicht.
|
|
49
|
-
if (proj.isImplicit)
|
|
50
|
-
continue;
|
|
60
|
+
for (const proj of liveProjectionsForEvent(event, registry)) {
|
|
51
61
|
const applyFn = proj.apply[event.type];
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
continue;
|
|
55
|
-
await applyFn(event, tx, proj.table);
|
|
62
|
+
if (applyFn)
|
|
63
|
+
await applyFn(event, tx, proj.table);
|
|
56
64
|
}
|
|
65
|
+
projectedEvents.add(event);
|
|
57
66
|
}
|
|
@@ -128,5 +128,5 @@ function respondRateLimited(c, decision, bucket) {
|
|
|
128
128
|
c.header("Retry-After", String(Math.max(1, decision.retryAfterSeconds)));
|
|
129
129
|
setRateLimitHeaders(c, decision);
|
|
130
130
|
const reqId = requestContext.get()?.requestId;
|
|
131
|
-
return c.json(serializeError(err, reqId), 429);
|
|
131
|
+
return c.json(serializeError(err, reqId, requestContext.get()?.errorDocs), 429);
|
|
132
132
|
}
|