@cosmicdrift/kumiko-framework 0.336.1 → 0.338.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/api-constants.d.ts +1 -0
- package/dist/api/api-constants.js +9 -0
- package/dist/api/auth-middleware.js +45 -21
- package/dist/api/auth-routes.d.ts +1 -0
- package/dist/api/auth-routes.js +20 -0
- package/dist/api/redis-sse-broker.d.ts +1 -0
- package/dist/api/redis-sse-broker.js +4 -5
- package/dist/api/server.js +14 -4
- package/dist/api/sse-route.js +14 -1
- package/dist/bun-db/query.js +6 -4
- package/dist/compliance/sub-processors.js +10 -1
- package/dist/crypto/kek-source.js +1 -0
- package/dist/db/blind-index-cleanup.d.ts +1 -0
- package/dist/db/blind-index-cleanup.js +12 -1
- package/dist/db/event-store-executor-read.js +30 -4
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/queries/event-consumer.d.ts +2 -2
- package/dist/db/queries/event-consumer.js +5 -4
- package/dist/db/queries/event-store.js +6 -6
- package/dist/db/queries/projection-rebuild.js +4 -4
- package/dist/db/queries/seed-context.js +6 -6
- package/dist/db/tenant-db.js +16 -8
- package/dist/derivatives/derivatives-context.js +5 -2
- package/dist/engine/boot-validator/access-declarations.js +13 -7
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +26 -0
- package/dist/engine/boot-validator/index.js +2 -1
- package/dist/engine/boot-validator/pii-retention.js +11 -1
- package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
- package/dist/engine/boot-validator/projection-list-screens.js +13 -0
- package/dist/engine/boot-validator/screens.js +12 -0
- package/dist/engine/build-config-feature-schema.js +4 -0
- package/dist/engine/config-helpers.d.ts +2 -1
- package/dist/engine/config-helpers.js +1 -0
- package/dist/engine/define-feature.js +1 -0
- package/dist/engine/extensions/escape-hatch-usage.js +3 -1
- package/dist/engine/feature-ast/extractors/hooks.js +7 -1
- 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/jobs-routes.js +6 -2
- package/dist/engine/feature-ast/extractors/round3.d.ts +2 -1
- package/dist/engine/feature-ast/extractors/round3.js +11 -0
- package/dist/engine/feature-ast/extractors/shared.js +1 -3
- package/dist/engine/feature-ast/index.d.ts +1 -1
- package/dist/engine/feature-ast/parse.js +3 -1
- package/dist/engine/feature-ast/patch.d.ts +3 -0
- package/dist/engine/feature-ast/patch.js +8 -11
- package/dist/engine/feature-ast/patcher.d.ts +6 -1
- package/dist/engine/feature-ast/patcher.js +3 -0
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +630 -1
- package/dist/engine/feature-ast/pattern-change-schema.js +19 -2
- package/dist/engine/feature-ast/patterns.d.ts +8 -2
- package/dist/engine/feature-ast/patterns.js +1 -0
- package/dist/engine/feature-ast/render.js +9 -0
- package/dist/engine/feature-builder-state.d.ts +2 -1
- package/dist/engine/feature-builder-state.js +1 -0
- package/dist/engine/feature-config-events-jobs.d.ts +4 -1
- package/dist/engine/feature-config-events-jobs.js +25 -1
- package/dist/engine/feature-manifest.d.ts +1 -0
- package/dist/engine/feature-manifest.js +1 -0
- package/dist/engine/feature-ui-extensions.js +24 -21
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/pattern-library/library.js +2 -1
- package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
- package/dist/engine/pattern-library/static-schemas.js +47 -4
- package/dist/engine/registry-facade.js +7 -0
- package/dist/engine/registry-ingest.js +7 -0
- package/dist/engine/registry-state.d.ts +2 -1
- package/dist/engine/registry-state.js +1 -0
- package/dist/engine/registry-validate.d.ts +1 -1
- package/dist/engine/registry-validate.js +39 -17
- package/dist/engine/registry.js +2 -2
- package/dist/engine/types/index.d.ts +3 -3
- package/dist/engine/types/index.js +1 -1
- package/dist/engine/types/screen.d.ts +1 -1
- package/dist/engine/types/screen.js +1 -1
- package/dist/errors/classes.d.ts +5 -0
- package/dist/errors/classes.js +15 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/i18n/required-surface-keys.js +3 -5
- package/dist/jobs/job-runner.js +2 -1
- package/dist/migrations/pending-rebuilds.d.ts +2 -0
- package/dist/migrations/pending-rebuilds.js +3 -0
- package/dist/pipeline/dispatch-query.js +3 -5
- package/dist/pipeline/dispatch-shared.js +32 -5
- package/dist/pipeline/dispatch-stream.js +2 -4
- package/dist/pipeline/dispatch-write.js +2 -4
- package/dist/pipeline/event-dispatcher-delivery.d.ts +0 -1
- package/dist/pipeline/event-dispatcher-delivery.js +1 -8
- package/dist/pipeline/event-dispatcher.js +16 -25
- package/dist/pipeline/handler-access-error.d.ts +3 -0
- package/dist/pipeline/handler-access-error.js +17 -0
- package/dist/pipeline/irreversible-operation-gate.js +9 -3
- package/dist/pipeline/member-read-only-transaction.js +8 -3
- package/dist/pipeline/system-hooks.js +7 -1
- package/dist/pipeline/system-identity-switch.js +11 -8
- package/dist/search/meilisearch-adapter.js +0 -2
- package/dist/stack/push-entity-projection-tables.js +5 -3
- package/dist/stack/redis.d.ts +1 -0
- package/dist/stack/redis.js +11 -2
- package/dist/stack/table-helpers.d.ts +3 -1
- package/dist/stack/table-helpers.js +9 -7
- package/dist/stack/test-stack.js +1 -7
- package/dist/testing/handler-context.js +3 -6
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.js +29 -2
- package/package.json +4 -4
- package/src/changes.json +153 -0
- package/src/scripts/codemod/collect-source-files.ts +48 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +3 -23
- package/src/scripts/codemod/migrate-db-raw.ts +6 -23
- package/src/scripts/codemod/migrate-open-to-all.ts +4 -24
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +47 -3
package/dist/engine/registry.js
CHANGED
|
@@ -3,7 +3,7 @@ import { dedupeFeatures } from "./dedupe-features.js";
|
|
|
3
3
|
import { buildRegistryFacade } from "./registry-facade.js";
|
|
4
4
|
import { populateClaimsAndAuth, populateConfigKeys, populateEvents, populateExtensionsAndSeeds, populateFeatureCore, populateHandlers, populateHooks, populateJobsAndNotifications, populateMetricsAndSecrets, populateProjectionsAndTables, populateScreensNavWorkspaces, populateTranslations, } from "./registry-ingest.js";
|
|
5
5
|
import { createInitialState } from "./registry-state.js";
|
|
6
|
-
import { applyExtensionUsages, autoWireSoftDeleteJobs, buildEventUpcasterChains, buildImplicitProjections, buildIncomingRelations, buildSearchableSortableCaches, computeHasRateLimitedHandler, finalizeWorkspaceNavMembership, populateHandlerEntityMappings, publishEventPiiCatalog, resolveNotificationTriggersAndRegisterHooks, validateBootGates, validateEntityHookTargets, validateEventMigrationVersions, validateExtensionSelectors, validateExtensionUsageTargets, validateFieldAccessHandlersAreEntityMapped, validateJobBackoff,
|
|
6
|
+
import { applyExtensionUsages, autoWireSoftDeleteJobs, buildEventUpcasterChains, buildImplicitProjections, buildIncomingRelations, buildSearchableSortableCaches, computeHasRateLimitedHandler, finalizeWorkspaceNavMembership, populateHandlerEntityMappings, publishEventPiiCatalog, resolveJobTriggers, resolveNotificationTriggersAndRegisterHooks, validateBootGates, validateEntityHookTargets, validateEventMigrationVersions, validateExtensionSelectors, validateExtensionUsageTargets, validateFieldAccessHandlersAreEntityMapped, validateJobBackoff, validateLifecycleHookTargets, validateLiveEntities, validateProjectionApplyKeys, validateRelationTargetsExist, validateRequiredFeatures, } from "./registry-validate.js";
|
|
7
7
|
// This is where the magic happens. By "magic" I mean: precomputed maps.
|
|
8
8
|
// I build everything once at boot (hooks, relations, searchable fields, ...)
|
|
9
9
|
// so nothing has to iterate over objects at runtime. O(1) instead of O(n*m).
|
|
@@ -41,7 +41,7 @@ export function createRegistry(rawFeatures) {
|
|
|
41
41
|
resolveNotificationTriggersAndRegisterHooks(state);
|
|
42
42
|
validateLifecycleHookTargets(state);
|
|
43
43
|
validateEntityHookTargets(state, features);
|
|
44
|
-
|
|
44
|
+
resolveJobTriggers(state);
|
|
45
45
|
validateBootGates(state);
|
|
46
46
|
validateLiveEntities(state);
|
|
47
47
|
validateJobBackoff(state);
|
|
@@ -1,10 +1,10 @@
|
|
|
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, 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";
|
|
2
|
+
export type { ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigBounds, ConfigCascade, ConfigCascadeLevel, ConfigComputedContext, ConfigComputedFn, ConfigDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, 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";
|
|
6
6
|
export type { ExtensionOptionsArgs, ExtensionOptionsFor, KumikoExtensionOptionsMap, } from "@cosmicdrift/kumiko-types/extension-options-map";
|
|
7
|
-
export type { BootCheckContext, BootCheckFn, FeatureDefinition, FeatureMetricDef, FeatureMetricType, FeatureRegistrar, MetricOptions, Registry, SecretKeyDefinition, SecretKeyHandle, SecretOptions, StoreTableDef, StoreTableEntry, StoreTableOptions, UiHintOption, UiHints, } from "@cosmicdrift/kumiko-types/feature";
|
|
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
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, NotifyFactory, NotifyFn, NotifyOptions, NotifyPriority, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, 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";
|
|
@@ -18,7 +18,7 @@ export type { FromRule, FromRuleKind, OwnershipClause, OwnershipMap, OwnershipRe
|
|
|
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
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
|
-
export { metricField } from "@cosmicdrift/kumiko-types/screen";
|
|
21
|
+
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } 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";
|
|
24
24
|
export type { WebSocketConnection, WebSocketMessageData, WebSocketRouteConnectDeps, WebSocketRouteDefinition, WebSocketSessionHandlers, } from "@cosmicdrift/kumiko-types/websocket-route";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export { isAgentVisibleScreen, resolveAgentExposure, } from "@cosmicdrift/kumiko-types/agent-exposure";
|
|
4
4
|
export { isOpenToAllGranted, isRateLimitDisabled, isUiAccessGranted, } from "@cosmicdrift/kumiko-types/handlers";
|
|
5
5
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-types/identifiers";
|
|
6
|
-
export { metricField } from "@cosmicdrift/kumiko-types/screen";
|
|
6
|
+
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
7
7
|
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, isFileField } from "../field-helpers.js";
|
|
8
8
|
export { resolveName, withResponseData } from "../handler-helpers.js";
|
|
9
9
|
export { HookPhases } from "../hook-helpers.js";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export type * from "@cosmicdrift/kumiko-types/screen";
|
|
2
|
-
export { metricField } from "@cosmicdrift/kumiko-types/screen";
|
|
2
|
+
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "@cosmicdrift/kumiko-types/screen";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { metricField } from "@cosmicdrift/kumiko-types/screen";
|
|
1
|
+
export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "@cosmicdrift/kumiko-types/screen";
|
package/dist/errors/classes.d.ts
CHANGED
|
@@ -24,6 +24,11 @@ export declare class AccessDeniedError extends KumikoError {
|
|
|
24
24
|
readonly httpStatus = 403;
|
|
25
25
|
constructor(opts?: Pick<ErrorOpts, "message" | "i18nKey" | "i18nParams" | "details" | "cause">);
|
|
26
26
|
}
|
|
27
|
+
export declare class UnauthenticatedError extends KumikoError {
|
|
28
|
+
readonly code = "unauthenticated";
|
|
29
|
+
readonly httpStatus = 401;
|
|
30
|
+
constructor(opts?: Pick<ErrorOpts, "message" | "details" | "cause">);
|
|
31
|
+
}
|
|
27
32
|
export type NotFoundDetails = {
|
|
28
33
|
readonly entity: string;
|
|
29
34
|
readonly id?: string;
|
package/dist/errors/classes.js
CHANGED
|
@@ -42,6 +42,21 @@ export class AccessDeniedError extends KumikoError {
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
// A caller without a signed-in principal (the synthesised anonymousAccess
|
|
46
|
+
// user) hit a handler that needs one. 401, not 403: an expired auth cookie is
|
|
47
|
+
// dropped by the browser, so this is how a lapsed session reaches the server.
|
|
48
|
+
export class UnauthenticatedError extends KumikoError {
|
|
49
|
+
code = "unauthenticated";
|
|
50
|
+
httpStatus = 401;
|
|
51
|
+
constructor(opts) {
|
|
52
|
+
super({
|
|
53
|
+
message: opts?.message ?? "this handler requires a signed-in user",
|
|
54
|
+
i18nKey: "auth.errors.missingToken",
|
|
55
|
+
...(opts?.details !== undefined && { details: opts.details }),
|
|
56
|
+
...(opts?.cause && { cause: opts.cause }),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
45
60
|
export class NotFoundError extends KumikoError {
|
|
46
61
|
code = "not_found";
|
|
47
62
|
httpStatus = 404;
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { FeatureDisabledDetails, FieldIssue, NotFoundDetails, PreconditionFailedDetails, RateLimitDetails, UnconfiguredDetails, UniqueViolationDetails, UnprocessableOpts, ValidationDetails, VersionConflictDetails, } from "./classes.js";
|
|
2
|
-
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
2
|
+
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
3
3
|
export type { ErrorCtorInput, ErrorOpts } from "./kumiko-error.js";
|
|
4
4
|
export { isKumikoError, KumikoError } from "./kumiko-error.js";
|
|
5
5
|
export { memberResolutionReadOnlyDenied } from "./member-resolution.js";
|
package/dist/errors/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
1
|
+
export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
|
|
2
2
|
export { isKumikoError, KumikoError } from "./kumiko-error.js";
|
|
3
3
|
export { memberResolutionReadOnlyDenied } from "./member-resolution.js";
|
|
4
4
|
export { AgentReasons, FrameworkReasons } from "./reasons.js";
|
|
@@ -144,6 +144,9 @@ function pushRelatedListSectionKeys(out, section) {
|
|
|
144
144
|
}
|
|
145
145
|
for (const action of section.rowActions ?? [])
|
|
146
146
|
pushRowActionKeys(out, action);
|
|
147
|
+
pushKey(out, section.groupBy?.label);
|
|
148
|
+
for (const label of Object.values(section.groupBy?.labels ?? {}))
|
|
149
|
+
pushKey(out, label);
|
|
147
150
|
}
|
|
148
151
|
function pushExpandableRowKeys(out, section) {
|
|
149
152
|
pushRelatedListSectionKeys(out, section);
|
|
@@ -330,11 +333,6 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
|
330
333
|
}
|
|
331
334
|
if (section.kind === "relatedList") {
|
|
332
335
|
pushRelatedListSectionKeys(out, section);
|
|
333
|
-
if (section.groupBy !== undefined) {
|
|
334
|
-
pushKey(out, section.groupBy.label);
|
|
335
|
-
for (const label of Object.values(section.groupBy.labels ?? {}))
|
|
336
|
-
pushKey(out, label);
|
|
337
|
-
}
|
|
338
336
|
continue;
|
|
339
337
|
}
|
|
340
338
|
if (isWriteFormEditSection(section)) {
|
package/dist/jobs/job-runner.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
|
|
1
2
|
import { Queue, UnrecoverableError, Worker } from "bullmq";
|
|
2
3
|
import { Redis } from "ioredis";
|
|
3
4
|
import { requestContext } from "../api/request-context.js";
|
|
@@ -703,7 +704,7 @@ export function createJobRunner(options) {
|
|
|
703
704
|
: undefined;
|
|
704
705
|
const jobDb = configDb
|
|
705
706
|
? createTenantDb(configDb, tenantId, "tenant", context.tracer, context.meter, undefined, {
|
|
706
|
-
unsafeRaw: jobDef.escapeHatch,
|
|
707
|
+
unsafeRaw: escapeHatchFor(jobDef.escapeHatch, "unsafeRaw"),
|
|
707
708
|
report: reportEscapeHatch,
|
|
708
709
|
...(jobPersonalDataGate && { personalDataGate: jobPersonalDataGate }),
|
|
709
710
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DbConnection } from "../db/connection.js";
|
|
2
2
|
import type { Registry } from "../engine/types/index.js";
|
|
3
3
|
import type { JobRunner } from "../jobs/index.js";
|
|
4
|
+
import type { Logger } from "../logging/types.js";
|
|
4
5
|
import { type RebuildResult } from "../pipeline/index.js";
|
|
5
6
|
export declare const pendingRebuildsTable: import("@cosmicdrift/kumiko-types/schema-table-types").SchemaTable;
|
|
6
7
|
export declare function createPendingRebuildsTable(db: DbConnection): Promise<void>;
|
|
@@ -56,6 +57,7 @@ export declare function rebuildProjectionOrMultiStream(name: string, deps: {
|
|
|
56
57
|
readonly errorPolicy?: {
|
|
57
58
|
readonly skipApplyErrors?: boolean;
|
|
58
59
|
};
|
|
60
|
+
readonly logger?: Pick<Logger, "error"> & Partial<Pick<Logger, "warn" | "debug">>;
|
|
59
61
|
}): Promise<RebuildResult>;
|
|
60
62
|
/** Logs a finished queue drain and throws when any projection failed; the
|
|
61
63
|
* failed tables stay queued. Shared by every `schema apply` entry point so the
|
|
@@ -72,6 +72,9 @@ export async function clearPendingRebuilds(db, rows) {
|
|
|
72
72
|
export async function rebuildProjectionOrMultiStream(name, deps) {
|
|
73
73
|
const { db, registry, signal, errorPolicy } = deps;
|
|
74
74
|
if (registry.getAllMultiStreamProjections().has(name)) {
|
|
75
|
+
if (errorPolicy?.skipApplyErrors === true) {
|
|
76
|
+
createFallbackLogger("migrations:pending-rebuilds", deps.logger).warn(`skipApplyErrors ignored for multi-stream projection "${name}"; declare msp.errorMode.rebuild instead`, { projection: name });
|
|
77
|
+
}
|
|
75
78
|
return rebuildMultiStreamProjection(name, {
|
|
76
79
|
db,
|
|
77
80
|
registry,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { hasAccess } from "../engine/access.js";
|
|
2
2
|
import { filterReadFields } from "../engine/field-access.js";
|
|
3
|
-
import {
|
|
3
|
+
import { NotFoundError, validationErrorFromZod } from "../errors/index.js";
|
|
4
4
|
import { assertNoSecretLeak } from "../secrets/index.js";
|
|
5
5
|
import { buildHandlerContext, enforceRateLimit, ensureFeatureEnabled, isMemberResolutionPrincipal, runHandlerInstrumented, } from "./dispatch-shared.js";
|
|
6
|
+
import { handlerAccessError } from "./handler-access-error.js";
|
|
6
7
|
import { runInMemberReadOnlyTransaction } from "./member-read-only-transaction.js";
|
|
7
8
|
// Standalone query execution — used by the public dispatcher.query() and
|
|
8
9
|
// by ctx.query/ctx.queryAs inside handlers. Runs the handler, applies
|
|
@@ -34,10 +35,7 @@ async function executeQueryInner(ctx, type, payload, user, origin, tx) {
|
|
|
34
35
|
// so in normal boots this branch shouldn't fire — the guard is belt-and-
|
|
35
36
|
// suspenders in case a handler sneaks through (e.g. runtime injection).
|
|
36
37
|
if (!hasAccess(user, handler.access)) {
|
|
37
|
-
throw
|
|
38
|
-
message: `access denied for ${type}`,
|
|
39
|
-
details: { handler: type },
|
|
40
|
-
});
|
|
38
|
+
throw handlerAccessError(user, type);
|
|
41
39
|
}
|
|
42
40
|
const parsed = handler.schema.safeParse(payload);
|
|
43
41
|
if (!parsed.success) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
|
|
1
2
|
import { requestContext, runWithOrigin } from "../api/request-context.js";
|
|
2
3
|
import { runInSavepoint, selectMany } from "../db/query.js";
|
|
3
4
|
import { createTenantDb, createUncheckedSystemDb, hasTenantColumn, } from "../db/tenant-db.js";
|
|
4
5
|
import { createDerivativesContext } from "../derivatives/derivatives-context.js";
|
|
6
|
+
import { createSystemUser } from "../engine/system-user.js";
|
|
5
7
|
import { isRateLimitDisabled, resolveAgentExposure } from "../engine/types/index.js";
|
|
6
|
-
import { FeatureDisabledError, InternalError, memberResolutionReadOnlyDenied, VersionConflictError, } from "../errors/index.js";
|
|
8
|
+
import { AccessDeniedError, FeatureDisabledError, InternalError, memberResolutionReadOnlyDenied, VersionConflictError, } from "../errors/index.js";
|
|
7
9
|
import { archiveStream as archiveStreamHelper, isStreamArchived, restoreStream as restoreStreamHelper, } from "../event-store/archive.js";
|
|
8
10
|
import { IdempotentAppendConflictError as EventStoreIdempotentAppendConflictError, VersionConflictError as EventStoreVersionConflictError, } from "../event-store/errors.js";
|
|
9
11
|
import { getStreamVersion, loadAggregate, loadAggregateAsOf, } from "../event-store/event-store.js";
|
|
@@ -148,7 +150,8 @@ export async function buildHandlerContext(ctx, type, user, origin, tx, afterComm
|
|
|
148
150
|
const handlerEscapeHatch = writeEscapeHatch ??
|
|
149
151
|
registry.getQueryHandler(type)?.escapeHatch ??
|
|
150
152
|
registry.getStreamHandler(type)?.escapeHatch;
|
|
151
|
-
const
|
|
153
|
+
const identitySwitchEscapeHatch = escapeHatchFor(handlerEscapeHatch, "systemIdentity");
|
|
154
|
+
const hasIdentitySwitchGrant = isSystem || identitySwitchEscapeHatch !== undefined;
|
|
152
155
|
const featureName = registry.getHandlerFeature(type);
|
|
153
156
|
const reportEscapeHatch = createEscapeHatchReporter({
|
|
154
157
|
handler: type,
|
|
@@ -158,11 +161,11 @@ export async function buildHandlerContext(ctx, type, user, origin, tx, afterComm
|
|
|
158
161
|
log: context.log,
|
|
159
162
|
window: ctx.escapeHatchReportWindow,
|
|
160
163
|
});
|
|
161
|
-
const identitySwitchGrantReason =
|
|
164
|
+
const identitySwitchGrantReason = identitySwitchEscapeHatch?.reason ??
|
|
162
165
|
(isSystem ? `r.systemScope() feature "${featureName ?? "unknown"}"` : undefined);
|
|
163
166
|
const buildTenantScopedDb = (source, signal) => createTenantDb(source, user.tenantId, isSystem ? "system" : "tenant", context.tracer, context.meter, signal, {
|
|
164
|
-
globalWrites: writeEscapeHatch,
|
|
165
|
-
unsafeRaw: handlerEscapeHatch,
|
|
167
|
+
globalWrites: escapeHatchFor(writeEscapeHatch, "globalWrites"),
|
|
168
|
+
unsafeRaw: escapeHatchFor(handlerEscapeHatch, "unsafeRaw"),
|
|
166
169
|
report: reportEscapeHatch,
|
|
167
170
|
memberReadOnly: isMemberResolutionPrincipal(user),
|
|
168
171
|
personalDataGate: buildPersonalDataGate(registry, origin),
|
|
@@ -212,6 +215,29 @@ export async function buildHandlerContext(ctx, type, user, origin, tx, afterComm
|
|
|
212
215
|
secrets: context.secrets,
|
|
213
216
|
})
|
|
214
217
|
: undefined;
|
|
218
|
+
// Cross-tenant accessor for SystemAdmin/system callers. The
|
|
219
|
+
// identity is the target tenant's system user so the caller's own user-scope
|
|
220
|
+
// values never resolve into another tenant's config; the db is tenant-mode
|
|
221
|
+
// scoped to the target.
|
|
222
|
+
const configFactory = context._configAccessorFactory;
|
|
223
|
+
const configFor = configFactory && dbSource
|
|
224
|
+
? (targetTenantId) => {
|
|
225
|
+
if (targetTenantId === user.tenantId && config)
|
|
226
|
+
return config;
|
|
227
|
+
if (!isSystemIdentity(user) && !user.roles.includes("SystemAdmin")) {
|
|
228
|
+
throw new AccessDeniedError({
|
|
229
|
+
message: "ctx.configFor(otherTenant) requires the system role or SystemAdmin",
|
|
230
|
+
details: { reason: "config_for_requires_system_admin" },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const targetSystemUser = createSystemUser(targetTenantId);
|
|
234
|
+
return configFactory({
|
|
235
|
+
user: { id: targetSystemUser.id, tenantId: targetTenantId },
|
|
236
|
+
db: createTenantDb(dbSource, targetTenantId, "tenant", context.tracer, context.meter),
|
|
237
|
+
secrets: context.secrets,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
: undefined;
|
|
215
241
|
// ctx.files resolved per-tenant through file-foundation (lazy — the
|
|
216
242
|
// provider is only resolved when a handle actually does I/O). Boot wires
|
|
217
243
|
// _fileProviderResolver when a file-provider plugin is mounted; falls back
|
|
@@ -584,6 +610,7 @@ export async function buildHandlerContext(ctx, type, user, origin, tx, afterComm
|
|
|
584
610
|
log,
|
|
585
611
|
notify,
|
|
586
612
|
...(config && { config }),
|
|
613
|
+
...(configFor && { configFor }),
|
|
587
614
|
...(files && { files }),
|
|
588
615
|
...(derivatives && { derivatives }),
|
|
589
616
|
// preSave hooks need `changes`/`previous`/`isNew`, which only exist once
|
|
@@ -3,6 +3,7 @@ import { hasAccess } from "../engine/access.js";
|
|
|
3
3
|
import { AccessDeniedError, memberResolutionReadOnlyDenied, NotFoundError, validationErrorFromZod, } from "../errors/index.js";
|
|
4
4
|
import { assertNoSecretLeak } from "../secrets/index.js";
|
|
5
5
|
import { buildHandlerContext, enforceRateLimit, ensureFeatureEnabled, isMemberResolutionPrincipal, runStreamInstrumented, } from "./dispatch-shared.js";
|
|
6
|
+
import { handlerAccessError } from "./handler-access-error.js";
|
|
6
7
|
// Standalone stream execution — used by the public dispatcher.stream().
|
|
7
8
|
// Chunk-by-chunk analog of executeQuery: same gate order (feature → rate-
|
|
8
9
|
// limit → access → validation → handler), but yields incrementally instead
|
|
@@ -25,10 +26,7 @@ async function* executeStreamInner(ctx, type, payload, user, origin) {
|
|
|
25
26
|
await ensureFeatureEnabled(ctx, type, user.tenantId);
|
|
26
27
|
await enforceRateLimit(ctx, handler.rateLimit, type, user, registry.isHandlerSystemScoped(type));
|
|
27
28
|
if (!hasAccess(user, handler.access)) {
|
|
28
|
-
throw
|
|
29
|
-
message: `access denied for ${type}`,
|
|
30
|
-
details: { handler: type },
|
|
31
|
-
});
|
|
29
|
+
throw handlerAccessError(user, type);
|
|
32
30
|
}
|
|
33
31
|
const parsed = handler.schema.safeParse(payload);
|
|
34
32
|
if (!parsed.success) {
|
|
@@ -13,6 +13,7 @@ import { AccessDeniedError, FrameworkReasons, InternalError, isKumikoError, memb
|
|
|
13
13
|
import { assertNoSecretLeak } from "../secrets/index.js";
|
|
14
14
|
import { buildHandlerContext, CONFIG_WRITE_RESET_TYPE, CONFIG_WRITE_SET_TYPE, checkFeatureEnabled, enforceRateLimit, isMemberResolutionPrincipal, resolveDbSource, runHandlerInstrumented, TENANT_TIMEZONE_CONFIG_KEY, } from "./dispatch-shared.js";
|
|
15
15
|
import { describeShape, extractNestedSpecs, isLifecycleResult, isWriteResultShape, prefixValidationPath, wrapToKumiko, } from "./dispatcher-utils.js";
|
|
16
|
+
import { handlerAccessError } from "./handler-access-error.js";
|
|
16
17
|
import { runProjections } from "./projections-runner.js";
|
|
17
18
|
function getTable(ctx, entityName) {
|
|
18
19
|
const { registry, tableCache } = ctx;
|
|
@@ -322,10 +323,7 @@ async function executeWriteInner(ctx, type, payload, user, origin, tx, afterComm
|
|
|
322
323
|
// so in normal boots this branch shouldn't fire — the guard is belt-and-
|
|
323
324
|
// suspenders in case a handler sneaks through (e.g. runtime injection).
|
|
324
325
|
if (!hasAccess(user, handler.access)) {
|
|
325
|
-
return writeFailure(
|
|
326
|
-
message: `access denied for ${type}`,
|
|
327
|
-
details: { handler: type },
|
|
328
|
-
}));
|
|
326
|
+
return writeFailure(handlerAccessError(user, type));
|
|
329
327
|
}
|
|
330
328
|
const parsed = handler.schema.safeParse(payload);
|
|
331
329
|
if (!parsed.success) {
|
|
@@ -67,5 +67,4 @@ export type PersistedConsumerOutcome = DeliveryOutcome & {
|
|
|
67
67
|
readonly pendingGaps: readonly PendingGapEntry[];
|
|
68
68
|
};
|
|
69
69
|
export declare function persistConsumerOutcome(tx: DbTx, name: string, instanceId: string, outcome: PersistedConsumerOutcome): Promise<void>;
|
|
70
|
-
export declare function persistConsumerPassFailure(tx: DbTx, name: string, instanceId: string, errorMessage: string): Promise<void>;
|
|
71
70
|
export declare function emitLagFromTx(tx: DbTx, consumerName: string, instanceId: string, cursor: bigint, meter: Meter): Promise<void>;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// resolve against (same #1438 dual-package-hazard pattern as event-store.ts).
|
|
5
5
|
import { Temporal as TemporalPolyfill } from "temporal-polyfill";
|
|
6
6
|
import { requestContext } from "../api/request-context.js";
|
|
7
|
-
import { insertConsumerIfAbsent, markConsumerProcessing, rearmDeadConsumer,
|
|
7
|
+
import { insertConsumerIfAbsent, markConsumerProcessing, rearmDeadConsumer, selectConsumerForUpdate, selectConsumerForUpdateSkipLocked, selectProvablyIdleConsumerPairs, updateConsumerDeliveryOutcome, } from "../db/queries/event-consumer.js";
|
|
8
8
|
import { selectEventsHeadId, selectPendingAndNewEventRows, } from "../db/queries/event-store.js";
|
|
9
9
|
import { coerceRow, extractTableInfo } from "../db/query.js";
|
|
10
10
|
import { qnScope } from "../engine/qualified-name.js";
|
|
@@ -319,13 +319,6 @@ function reportSkippedEvent(consumer, eventId, e, errMessage, context) {
|
|
|
319
319
|
export async function persistConsumerOutcome(tx, name, instanceId, outcome) {
|
|
320
320
|
await updateConsumerDeliveryOutcome(tx, name, instanceId, outcome);
|
|
321
321
|
}
|
|
322
|
-
// Best-effort record of a pass that threw before persistConsumerOutcome
|
|
323
|
-
// could run (event-dispatcher.ts's processConsumer catch). The caller opens
|
|
324
|
-
// its OWN transaction for this — the one that rolled back never committed
|
|
325
|
-
// anything, including this write, if it happened inside the same tx.
|
|
326
|
-
export async function persistConsumerPassFailure(tx, name, instanceId, errorMessage) {
|
|
327
|
-
await recordConsumerPassFailure(tx, name, instanceId, errorMessage);
|
|
328
|
-
}
|
|
329
322
|
// Emit the lag gauge inside the consumer pass's tx so ops sees a snapshot
|
|
330
323
|
// consistent with the cursor we just advanced to. `MAX(id)` on the events
|
|
331
324
|
// table is an O(1) reverse-index scan — cheap even under load.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { selectSnapshotXmax, selectSnapshotXmin } from "../db/queries/event-consumer.js";
|
|
1
|
+
import { recordConsumerPassFailure, selectSnapshotXmax, selectSnapshotXmin, } from "../db/queries/event-consumer.js";
|
|
2
2
|
import { SYSTEM_TENANT_ID } from "../engine/types/identifiers.js";
|
|
3
3
|
import { EVENTS_PUBSUB_CHANNEL } from "../event-store/index.js";
|
|
4
4
|
import { emitEventConsumerPassOutcome, emitEventConsumerPassSkipped, emitEventConsumerRearmExhausted, emitEventDispatcherListenConnected, getFallbackMeter, getFallbackTracer, } from "../observability/index.js";
|
|
5
5
|
import { SHARED_INSTANCE_SENTINEL } from "./event-consumer-state.js";
|
|
6
|
-
import { acquireConsumerState, consumerInstanceId, deliverEvents, emitLagFromTx, fetchPendingEvents, markProcessing, persistConsumerOutcome,
|
|
6
|
+
import { acquireConsumerState, consumerInstanceId, deliverEvents, emitLagFromTx, fetchPendingEvents, markProcessing, persistConsumerOutcome, preRegisterConsumers, selectIdleConsumerKeys, } from "./event-dispatcher-delivery.js";
|
|
7
7
|
import { partitionBurntGaps, splitRangeExcludingIds, toIdRanges } from "./pending-gap-ranges.js";
|
|
8
8
|
const DEFAULT_BATCH_SIZE = 200;
|
|
9
9
|
const DEFAULT_POLL_MS = 100;
|
|
@@ -35,6 +35,15 @@ export function createEventDispatcher(options) {
|
|
|
35
35
|
}
|
|
36
36
|
const tracer = options.tracer ?? getFallbackTracer();
|
|
37
37
|
const meter = options.meter ?? getFallbackMeter();
|
|
38
|
+
// A standalone dispatcher has no logger on its context; console.error keeps the failure in pod logs.
|
|
39
|
+
const logDispatcherError = (message) => {
|
|
40
|
+
if (context.log) {
|
|
41
|
+
context.log.error(message);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.error(message);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
38
47
|
// Tracks which (consumer, instanceId) pairs already fired
|
|
39
48
|
// kumiko_event_consumer_rearm_exhausted_total, so a consumer stuck dead
|
|
40
49
|
// across many poll passes emits the ops-signal once, not every pass
|
|
@@ -220,7 +229,7 @@ export function createEventDispatcher(options) {
|
|
|
220
229
|
//
|
|
221
230
|
// A failure here (e.g. a transient DB blip) must not abort the whole
|
|
222
231
|
// pass — timer/LISTEN callers swallow doPass()'s rejection silently
|
|
223
|
-
// (see start()), which would mean no log, no
|
|
232
|
+
// (see start()), which would mean no log, no recordConsumerPassFailure,
|
|
224
233
|
// and no backoff for every consumer this tick. Falling back to "nothing
|
|
225
234
|
// proven idle" instead routes every consumer through its normal
|
|
226
235
|
// acquireConsumerState path, whose own try/catch already covers this.
|
|
@@ -254,12 +263,7 @@ export function createEventDispatcher(options) {
|
|
|
254
263
|
emitEventDispatcherListenConnected(meter, false);
|
|
255
264
|
const msg = e instanceof Error ? e.message : String(e);
|
|
256
265
|
const logMsg = `[event-dispatcher] idle pre-check failed, falling back to per-consumer locking every tick until it recovers: ${msg}`;
|
|
257
|
-
|
|
258
|
-
context.log.error(logMsg);
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
console.error(logMsg);
|
|
262
|
-
}
|
|
266
|
+
logDispatcherError(logMsg);
|
|
263
267
|
}
|
|
264
268
|
}
|
|
265
269
|
// Every consumer runs its own turn concurrently (own TX, own cursor,
|
|
@@ -396,15 +400,7 @@ export function createEventDispatcher(options) {
|
|
|
396
400
|
// callers and at-least-once-with-duplicate-delivery on the next pass;
|
|
397
401
|
// neither is what we want, so ops needs to see it.
|
|
398
402
|
const msg = e instanceof Error ? e.message : String(e);
|
|
399
|
-
|
|
400
|
-
context.log.error(`[event-dispatcher] ${consumer.name} pass failed: ${msg}`);
|
|
401
|
-
}
|
|
402
|
-
else {
|
|
403
|
-
// No logger wired onto this context (e.g. createEventDispatcher used
|
|
404
|
-
// standalone) — console.error is the only way ops still sees this in
|
|
405
|
-
// pod logs instead of the failure vanishing with the rolled-back tx.
|
|
406
|
-
console.error(`[event-dispatcher] ${consumer.name} pass failed: ${msg}`);
|
|
407
|
-
}
|
|
403
|
+
logDispatcherError(`[event-dispatcher] ${consumer.name} pass failed: ${msg}`);
|
|
408
404
|
span.setStatus("error", msg);
|
|
409
405
|
// The whole pass rolled back, so markProcessing/persistConsumerOutcome
|
|
410
406
|
// never committed — attempts/last_error are stuck at their pre-pass
|
|
@@ -413,18 +409,13 @@ export function createEventDispatcher(options) {
|
|
|
413
409
|
// so any failure here is swallowed after logging.
|
|
414
410
|
try {
|
|
415
411
|
await db.begin(async (tx) => {
|
|
416
|
-
await
|
|
412
|
+
await recordConsumerPassFailure(tx, consumer.name, instanceId, msg);
|
|
417
413
|
});
|
|
418
414
|
}
|
|
419
415
|
catch (persistErr) {
|
|
420
416
|
const persistMsg = persistErr instanceof Error ? persistErr.message : String(persistErr);
|
|
421
417
|
const failureMsg = `[event-dispatcher] ${consumer.name} failed to persist pass-failure state: ${persistMsg}`;
|
|
422
|
-
|
|
423
|
-
context.log.error(failureMsg);
|
|
424
|
-
}
|
|
425
|
-
else {
|
|
426
|
-
console.error(failureMsg);
|
|
427
|
-
}
|
|
418
|
+
logDispatcherError(failureMsg);
|
|
428
419
|
}
|
|
429
420
|
// Escalate the backoff so this consumer isn't retried on the very
|
|
430
421
|
// next tick — see PASS_FAILURE_BASE_BACKOFF_MS/MAX_MS above.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isAnonymousSessionUser } from "../engine/system-user.js";
|
|
2
|
+
import { AccessDeniedError, UnauthenticatedError } from "../errors/index.js";
|
|
3
|
+
// Anonymous callers get 401 so a client whose session lapsed (browser dropped
|
|
4
|
+
// the expired auth cookie) can tell "sign in again" apart from "signed in but
|
|
5
|
+
// not allowed", which stays 403.
|
|
6
|
+
export function handlerAccessError(user, type) {
|
|
7
|
+
if (isAnonymousSessionUser(user)) {
|
|
8
|
+
return new UnauthenticatedError({
|
|
9
|
+
message: `${type} requires a signed-in user`,
|
|
10
|
+
details: { handler: type },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return new AccessDeniedError({
|
|
14
|
+
message: `access denied for ${type}`,
|
|
15
|
+
details: { handler: type },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -28,11 +28,17 @@ function assertEntryHandlerHighRisk(args) {
|
|
|
28
28
|
},
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
+
// Stream handlers have no agent hint, so the "declare risk high" advice cannot apply to them.
|
|
32
|
+
function highRiskRemedy(entryHandler) {
|
|
33
|
+
return entryHandler.qn.includes(":stream:")
|
|
34
|
+
? 'stream handlers cannot declare an agent risk and always resolve "mid" — dispatch a risk "high" write handler directly instead'
|
|
35
|
+
: `declare agent: { risk: "high" } on "${entryHandler.qn}"`;
|
|
36
|
+
}
|
|
31
37
|
export function assertIrreversibleOperationAllowed(operation) {
|
|
32
38
|
assertEntryHandlerHighRisk({
|
|
33
39
|
reason: FrameworkReasons.irreversibleOperationRequiresHighRisk,
|
|
34
40
|
buildMessage: (entryHandler) => `Handler "${entryHandler.qn}" performs an irreversible ${operation} but resolves agent risk ` +
|
|
35
|
-
`"${entryHandler.risk}" —
|
|
41
|
+
`"${entryHandler.risk}" — ${highRiskRemedy(entryHandler)}. The ` +
|
|
36
42
|
"directly called handler must carry it; delegating via ctx.write/writeAs does not.",
|
|
37
43
|
details: { operation },
|
|
38
44
|
});
|
|
@@ -49,8 +55,8 @@ export function assertInstructionFieldWriteAllowed(entityName, verb, writtenFiel
|
|
|
49
55
|
assertEntryHandlerHighRisk({
|
|
50
56
|
reason: FrameworkReasons.instructionFieldWriteRequiresHighRisk,
|
|
51
57
|
buildMessage: (entryHandler) => `Handler "${entryHandler.qn}" ${verb}s "${entityName}" field(s) ${fields.join(", ")} ` +
|
|
52
|
-
`flagged readAsInstruction: true but resolves agent risk "${entryHandler.risk}" —
|
|
53
|
-
|
|
58
|
+
`flagged readAsInstruction: true but resolves agent risk "${entryHandler.risk}" — ` +
|
|
59
|
+
`${highRiskRemedy(entryHandler)}. The directly called handler must carry ` +
|
|
54
60
|
"it; delegating via ctx.write/writeAs does not.",
|
|
55
61
|
details: { entityName, verb, fields },
|
|
56
62
|
});
|
|
@@ -6,8 +6,13 @@ const PG_READ_ONLY_SQLSTATE = "25006";
|
|
|
6
6
|
function isReadOnlyTransactionViolation(e) {
|
|
7
7
|
return extractPgError(e)?.code === PG_READ_ONLY_SQLSTATE;
|
|
8
8
|
}
|
|
9
|
+
class SavepointDiscard extends Error {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("savepoint discarded on purpose");
|
|
12
|
+
this.name = "SavepointDiscard";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
9
15
|
async function runInDiscardedReadOnlySavepoint(tx, fn) {
|
|
10
|
-
const discardMarker = {};
|
|
11
16
|
const settled = {};
|
|
12
17
|
try {
|
|
13
18
|
await runInSavepoint(tx, async (sp) => {
|
|
@@ -16,11 +21,11 @@ async function runInDiscardedReadOnlySavepoint(tx, fn) {
|
|
|
16
21
|
// @cast-boundary driver savepoint handle — structurally a DbTx, same shape runInSavepoint's caller relies on
|
|
17
22
|
settled.outcome = { value: await fn(sp) };
|
|
18
23
|
// Always discarded: RELEASE SAVEPOINT would leave the outer tx read-only too.
|
|
19
|
-
throw
|
|
24
|
+
throw new SavepointDiscard();
|
|
20
25
|
});
|
|
21
26
|
}
|
|
22
27
|
catch (e) {
|
|
23
|
-
if (e
|
|
28
|
+
if (!(e instanceof SavepointDiscard))
|
|
24
29
|
throw e;
|
|
25
30
|
}
|
|
26
31
|
if (!settled.outcome) {
|
|
@@ -204,7 +204,13 @@ async function readProjectionRowForSearch(ctx, registry, entityName, entity, ten
|
|
|
204
204
|
const row = await createTenantDb(baseDb, tenantId).fetchOne(table, where);
|
|
205
205
|
if (!row)
|
|
206
206
|
return undefined;
|
|
207
|
-
|
|
207
|
+
const state = rehydrateCompoundTypes(row, entity);
|
|
208
|
+
// The event payload never carries sensitive fields; extensions must not see them via the row either.
|
|
209
|
+
for (const [name, field] of Object.entries(entity.fields)) {
|
|
210
|
+
if ("sensitive" in field && field.sensitive === true)
|
|
211
|
+
delete state[name];
|
|
212
|
+
}
|
|
213
|
+
return state;
|
|
208
214
|
}
|
|
209
215
|
// #1610 — subject-annotated searchable fields are ciphertext in the event
|
|
210
216
|
// payload; decrypt into the derived index only. No KMS → omit ciphertext
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
|
|
1
2
|
import { withSystemDbUnsafeRawGrant, withUnsafeRawGrant, } from "../db/tenant-db.js";
|
|
2
3
|
import { SYSTEM_ROLE, SYSTEM_USER_ID } from "../engine/system-user.js";
|
|
3
4
|
import { AccessDeniedError, FrameworkReasons, InternalError } from "../errors/index.js";
|
|
@@ -13,7 +14,7 @@ function hasSameClaims(caller, asUser) {
|
|
|
13
14
|
if (keys.length !== Object.keys(asUserClaims).length)
|
|
14
15
|
return false;
|
|
15
16
|
return keys.every((key) => Object.hasOwn(asUserClaims, key) &&
|
|
16
|
-
|
|
17
|
+
Bun.deepEquals(callerClaims[key], asUserClaims[key], true));
|
|
17
18
|
}
|
|
18
19
|
// claims drive ownership row filters and origin makes a ctx read-only, so both must match exactly.
|
|
19
20
|
export function isSelfDelegation(caller, asUser) {
|
|
@@ -212,6 +213,8 @@ export function withHookEscapeHatchGrant(context, callerLabel, escapeHatch) {
|
|
|
212
213
|
const ctxDb = readDbLikeValue(context, "db");
|
|
213
214
|
const ctxDbOutsideTransaction = readDbLikeValue(context, "dbOutsideTransaction");
|
|
214
215
|
const ctxSystemDb = readDbLikeValue(context, "systemDb");
|
|
216
|
+
const identityHatch = escapeHatchFor(escapeHatch, "systemIdentity");
|
|
217
|
+
const rawHatch = escapeHatchFor(escapeHatch, "unsafeRaw");
|
|
215
218
|
if (!ctxQueryAs &&
|
|
216
219
|
!ctxWriteAs &&
|
|
217
220
|
!ctxResolveActiveMembership &&
|
|
@@ -224,21 +227,21 @@ export function withHookEscapeHatchGrant(context, callerLabel, escapeHatch) {
|
|
|
224
227
|
}
|
|
225
228
|
return {
|
|
226
229
|
...context,
|
|
227
|
-
...gatedIdentitySwitchFields(callerLabel,
|
|
230
|
+
...gatedIdentitySwitchFields(callerLabel, identityHatch, ctxQueryAs, ctxWriteAs),
|
|
228
231
|
...(ctxResolveActiveMembership &&
|
|
229
|
-
|
|
232
|
+
identityHatch === undefined && {
|
|
230
233
|
resolveActiveMembership: deniedResolveActiveMembership(callerLabel),
|
|
231
234
|
}),
|
|
232
|
-
...gatedMemberReaderField(callerLabel,
|
|
233
|
-
...gatedProjectionReaderField(callerLabel,
|
|
235
|
+
...gatedMemberReaderField(callerLabel, identityHatch, ctxQueryAsMember),
|
|
236
|
+
...gatedProjectionReaderField(callerLabel, identityHatch, ctxQueryProjection),
|
|
234
237
|
// @cast-boundary engine-bridge — withUnsafeRawGrant passes non-TenantDb values (e.g. a guard Proxy) through unchanged.
|
|
235
|
-
...(ctxDb && { db: withUnsafeRawGrant(ctxDb,
|
|
238
|
+
...(ctxDb && { db: withUnsafeRawGrant(ctxDb, rawHatch) }),
|
|
236
239
|
...(ctxDbOutsideTransaction && {
|
|
237
|
-
dbOutsideTransaction: withUnsafeRawGrant(ctxDbOutsideTransaction,
|
|
240
|
+
dbOutsideTransaction: withUnsafeRawGrant(ctxDbOutsideTransaction, rawHatch),
|
|
238
241
|
}),
|
|
239
242
|
// @cast-boundary engine-bridge — withSystemDbUnsafeRawGrant passes non-UncheckedSystemDb values through unchanged.
|
|
240
243
|
...(ctxSystemDb && {
|
|
241
|
-
systemDb: withSystemDbUnsafeRawGrant(ctxSystemDb,
|
|
244
|
+
systemDb: withSystemDbUnsafeRawGrant(ctxSystemDb, rawHatch, callerLabel),
|
|
242
245
|
}),
|
|
243
246
|
};
|
|
244
247
|
}
|