@cosmicdrift/kumiko-framework 0.340.0 → 0.342.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/api/index.d.ts +2 -1
  2. package/dist/api/index.js +1 -1
  3. package/dist/api/route-registrars.d.ts +4 -2
  4. package/dist/api/route-registrars.js +9 -1
  5. package/dist/api/server.d.ts +2 -1
  6. package/dist/api/server.js +43 -5
  7. package/dist/crypto/kek-source.d.ts +3 -4
  8. package/dist/crypto/kek-source.js +2 -5
  9. package/dist/crypto/kms-wiring.d.ts +2 -2
  10. package/dist/crypto/kms-wiring.js +2 -2
  11. package/dist/db/index.d.ts +2 -1
  12. package/dist/db/index.js +2 -1
  13. package/dist/db/migrate-generator.d.ts +6 -0
  14. package/dist/db/migrate-generator.js +15 -1
  15. package/dist/db/migrate-runner.d.ts +1 -0
  16. package/dist/db/migrate-runner.js +11 -1
  17. package/dist/db/queries/shadow-swap.d.ts +2 -1
  18. package/dist/db/queries/shadow-swap.js +70 -7
  19. package/dist/db/retired-framework-tables.d.ts +10 -0
  20. package/dist/db/retired-framework-tables.js +72 -0
  21. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  22. package/dist/engine/boot-validator/entity-handler.js +48 -0
  23. package/dist/engine/define-handler.js +1 -0
  24. package/dist/engine/feature-ast/extractors/handlers.d.ts +2 -1
  25. package/dist/engine/feature-ast/extractors/handlers.js +11 -1
  26. package/dist/engine/feature-ast/extractors/hooks.d.ts +2 -1
  27. package/dist/engine/feature-ast/extractors/hooks.js +14 -0
  28. package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
  29. package/dist/engine/feature-ast/extractors/index.js +1 -1
  30. package/dist/engine/feature-ast/patch.js +1 -0
  31. package/dist/engine/feature-ast/pattern-change-schema.d.ts +20 -0
  32. package/dist/engine/feature-ast/pattern-change-schema.js +10 -0
  33. package/dist/engine/feature-ast/patterns.d.ts +2 -1
  34. package/dist/engine/feature-ast/render.js +3 -0
  35. package/dist/engine/feature-entity-handlers.d.ts +2 -1
  36. package/dist/engine/feature-entity-handlers.js +4 -0
  37. package/dist/engine/feature-ui-extensions.js +9 -0
  38. package/dist/engine/index.d.ts +1 -1
  39. package/dist/engine/membership-roles.js +5 -1
  40. package/dist/engine/pattern-library/mixed-schemas.js +5 -0
  41. package/dist/engine/registry-state.js +38 -5
  42. package/dist/engine/registry-validate.js +2 -0
  43. package/dist/engine/soft-delete-cleanup.d.ts +2 -0
  44. package/dist/engine/soft-delete-cleanup.js +28 -9
  45. package/dist/engine/tier-resolver-extension.d.ts +2 -0
  46. package/dist/engine/types/index.d.ts +2 -2
  47. package/dist/entrypoint/index.js +25 -6
  48. package/dist/errors/classes.d.ts +5 -0
  49. package/dist/errors/classes.js +13 -0
  50. package/dist/errors/index.d.ts +1 -1
  51. package/dist/errors/index.js +1 -1
  52. package/dist/files/file-routes.js +7 -2
  53. package/dist/files/index.d.ts +1 -0
  54. package/dist/files/index.js +1 -0
  55. package/dist/files/provider-resolver.d.ts +2 -0
  56. package/dist/files/provider-resolver.js +16 -1
  57. package/dist/files/resolve-content-type.d.ts +11 -0
  58. package/dist/files/resolve-content-type.js +71 -0
  59. package/dist/files/types.d.ts +1 -1
  60. package/dist/files/types.js +5 -1
  61. package/dist/jobs/job-runner.js +8 -2
  62. package/dist/observability/escape-hatch-report.d.ts +10 -1
  63. package/dist/observability/escape-hatch-report.js +19 -1
  64. package/dist/observability/index.d.ts +2 -2
  65. package/dist/observability/index.js +2 -2
  66. package/dist/observability/standard-metrics.d.ts +1 -0
  67. package/dist/observability/standard-metrics.js +14 -0
  68. package/dist/pipeline/dispatch-shared.d.ts +4 -1
  69. package/dist/pipeline/dispatch-shared.js +70 -9
  70. package/dist/pipeline/dispatch-write.js +35 -10
  71. package/dist/pipeline/dispatcher.d.ts +2 -0
  72. package/dist/pipeline/dispatcher.js +17 -1
  73. package/dist/pipeline/projection-rebuild.js +1 -1
  74. package/dist/rate-limit/bucket.d.ts +4 -0
  75. package/dist/rate-limit/bucket.js +25 -0
  76. package/dist/rate-limit/index.d.ts +2 -2
  77. package/dist/rate-limit/index.js +2 -2
  78. package/dist/rate-limit/middleware.d.ts +9 -0
  79. package/dist/rate-limit/middleware.js +33 -2
  80. package/dist/rate-limit/resolver.js +2 -1
  81. package/dist/redis/cache-sync-bus.d.ts +24 -0
  82. package/dist/redis/cache-sync-bus.js +152 -0
  83. package/dist/redis/cache-sync-topics.d.ts +17 -0
  84. package/dist/redis/cache-sync-topics.js +22 -0
  85. package/dist/redis/index.d.ts +4 -0
  86. package/dist/redis/index.js +2 -0
  87. package/dist/redis/pubsub-signal.d.ts +1 -0
  88. package/dist/redis/pubsub-signal.js +28 -0
  89. package/dist/schema-cli.d.ts +8 -6
  90. package/dist/schema-cli.js +37 -4
  91. package/dist/stack/redis.d.ts +6 -1
  92. package/dist/stack/redis.js +7 -2
  93. package/dist/stack/test-stack.d.ts +15 -0
  94. package/dist/stack/test-stack.js +21 -1
  95. package/package.json +4 -4
  96. package/src/changes.json +75 -0
@@ -7,7 +7,7 @@ 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, 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";
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
11
  export { 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";
@@ -15,7 +15,7 @@ export type { EntityId, TenantId } from "@cosmicdrift/kumiko-types/identifiers";
15
15
  export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-types/identifiers";
16
16
  export type { ContentCollectionDefinition, NavDefinition, NavIconKey, } from "@cosmicdrift/kumiko-types/nav";
17
17
  export type { FromRule, FromRuleKind, OwnershipClause, OwnershipMap, OwnershipRef, OwnershipRule, SqlFragment, WhereRule, WhereRuleContext, } from "@cosmicdrift/kumiko-types/ownership";
18
- export type { EntityProjectionExtension, MspErrorMode, MspErrorPolicy, MultiStreamApplyFn, MultiStreamProjectionDefinition, ProjectionDefinition, ProjectionTable, SingleStreamApplyFn, } from "@cosmicdrift/kumiko-types/projection";
18
+ export type { EntityProjectionExtension, MspErrorMode, MspErrorPolicy, MultiStreamApplyFn, MultiStreamProjectionDefinition, ProjectionDefinition, ProjectionRowIdOf, 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
21
  export { metricField, relatedListGroupHeaderLabel, relatedListGroupKey, } from "@cosmicdrift/kumiko-types/screen";
@@ -45,6 +45,7 @@ import { createJobRunner } from "../jobs/job-runner.js";
45
45
  import { createLifecycle } from "../lifecycle/index.js";
46
46
  import { createNoopProvider } from "../observability/index.js";
47
47
  import { dispatcherToWriteRef } from "../pipeline/dispatcher.js";
48
+ import { resolveCacheSyncBus } from "../redis/cache-sync-bus.js";
48
49
  // --- Shared builders ------------------------------------------------------
49
50
  //
50
51
  // Three factories, three near-identical buildServer() + createJobRunner()
@@ -71,6 +72,24 @@ function mergeDispatcherOptions(caller, jobRunner) {
71
72
  function resolveObservability(observability) {
72
73
  return observability ?? createNoopProvider();
73
74
  }
75
+ // One bus per process, decided before the file-provider resolver is built so
76
+ // the resolver subscribes to it; buildServer then receives it explicitly and
77
+ // does not create a second one.
78
+ function resolveEntrypointCacheSync(options, lifecycle) {
79
+ const { bus, owned } = resolveCacheSyncBus(options.dispatcherOptions?.cacheSync);
80
+ if (owned) {
81
+ lifecycle.registerShutdownHook("cacheSync", async () => {
82
+ await owned.close();
83
+ });
84
+ }
85
+ return {
86
+ options: {
87
+ ...options,
88
+ dispatcherOptions: { ...options.dispatcherOptions, cacheSync: bus ?? null },
89
+ },
90
+ context: withFileProviderResolver(options.registry, options.context, bus),
91
+ };
92
+ }
74
93
  function contextWithObservability(context, observability, effectiveFeatures) {
75
94
  return {
76
95
  ...context,
@@ -177,7 +196,7 @@ function requireDispatcher(server, mode) {
177
196
  export function createApiEntrypoint(options) {
178
197
  const lifecycle = options.lifecycle ?? createLifecycle({ startReady: true });
179
198
  const observability = resolveObservability(options.observability);
180
- const context = withFileProviderResolver(options.registry, options.context);
199
+ const { options: busOptions, context } = resolveEntrypointCacheSync(options, lifecycle);
181
200
  // Boot-validation (Welle 2.6.c) — fail loud before traffic arrives:
182
201
  // (a) Any jobs declared + no jobs-block → command-dispatcher would
183
202
  // silently drop every event-triggered enqueue. Fix: add jobs:
@@ -211,7 +230,7 @@ export function createApiEntrypoint(options) {
211
230
  // processLane is "both": worker-lane MSPs must run here or they'd never
212
231
  // apply anywhere.
213
232
  const { runLocal: runLocalDispatcher, ...dispatcherTunables } = options.eventDispatcher ?? {};
214
- const server = buildApiServer({ ...options, context }, lifecycle, runLocalDispatcher ? dispatcherTunables : { disabled: true }, apiJobRunner, runLocalDispatcher ? "both" : "api");
233
+ const server = buildApiServer({ ...busOptions, context }, lifecycle, runLocalDispatcher ? dispatcherTunables : { disabled: true }, apiJobRunner, runLocalDispatcher ? "both" : "api");
215
234
  apiJobRunner?.attachDispatcher(dispatcherToWriteRef(server.dispatcher));
216
235
  return {
217
236
  app: server.app,
@@ -243,9 +262,9 @@ export function createApiEntrypoint(options) {
243
262
  export function createWorkerEntrypoint(options) {
244
263
  const lifecycle = options.lifecycle ?? createLifecycle({ startReady: true });
245
264
  const observability = resolveObservability(options.observability);
246
- const context = withFileProviderResolver(options.registry, options.context);
265
+ const { options: busOptions, context } = resolveEntrypointCacheSync(options, lifecycle);
247
266
  const jobRunner = buildJobRunnerWithHook(options.registry, contextWithObservability(context, observability, options.dispatcherOptions?.effectiveFeatures), options, "worker", lifecycle, "jobRunner");
248
- const server = buildWorkerServer({ ...options, context }, lifecycle, jobRunner);
267
+ const server = buildWorkerServer({ ...busOptions, context }, lifecycle, jobRunner);
249
268
  jobRunner.attachDispatcher(dispatcherToWriteRef(server.dispatcher));
250
269
  const eventDispatcher = requireDispatcher(server, "worker");
251
270
  return {
@@ -268,7 +287,7 @@ export function createWorkerEntrypoint(options) {
268
287
  export function createAllInOneEntrypoint(options) {
269
288
  const lifecycle = options.lifecycle ?? createLifecycle({ startReady: true });
270
289
  const observability = resolveObservability(options.observability);
271
- const context = withFileProviderResolver(options.registry, options.context);
290
+ const { options: busOptions, context } = resolveEntrypointCacheSync(options, lifecycle);
272
291
  const jobRunnerContext = contextWithObservability(context, observability, options.dispatcherOptions?.effectiveFeatures);
273
292
  // All-in-one consumes BOTH lanes: two runners, each with a BullMQ worker
274
293
  // for its own lane's queue. Both runners hold queue-clients for both
@@ -286,7 +305,7 @@ export function createAllInOneEntrypoint(options) {
286
305
  // every MSP (api-only, worker-only, both) must run here. `runLocal` is
287
306
  // the API-mode flag — all-in-one is always local, strip it.
288
307
  const { runLocal: _runLocal, ...allInOneDispatcherTunables } = options.eventDispatcher ?? {};
289
- const server = buildApiServer({ ...options, context }, lifecycle, allInOneDispatcherTunables, workerJobRunner, "both");
308
+ const server = buildApiServer({ ...busOptions, context }, lifecycle, allInOneDispatcherTunables, workerJobRunner, "both");
290
309
  workerJobRunner.attachDispatcher(dispatcherToWriteRef(server.dispatcher));
291
310
  apiJobRunner.attachDispatcher(dispatcherToWriteRef(server.dispatcher));
292
311
  const eventDispatcher = requireDispatcher(server, "all-in-one");
@@ -112,3 +112,8 @@ export declare class RateLimitError extends KumikoError {
112
112
  readonly details: RateLimitDetails;
113
113
  constructor(details: RateLimitDetails, opts?: Pick<ErrorOpts, "i18nKey" | "cause">);
114
114
  }
115
+ export declare class RateLimitUnavailableError extends KumikoError {
116
+ readonly code = "rate_limit_unavailable";
117
+ readonly httpStatus = 503;
118
+ constructor(opts?: Pick<ErrorOpts, "cause">);
119
+ }
@@ -203,3 +203,16 @@ export class RateLimitError extends KumikoError {
203
203
  this.details = details;
204
204
  }
205
205
  }
206
+ // The rate-limit backend (Redis) failed, so no decision could be made. The
207
+ // request is refused (fail-closed) with 503 instead of leaking a generic 500.
208
+ export class RateLimitUnavailableError extends KumikoError {
209
+ code = "rate_limit_unavailable";
210
+ httpStatus = 503;
211
+ constructor(opts) {
212
+ super({
213
+ message: "rate limiter unavailable",
214
+ i18nKey: "errors.rate_limit_unavailable",
215
+ ...(opts?.cause && { cause: opts.cause }),
216
+ });
217
+ }
218
+ }
@@ -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, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
2
+ export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, 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";
@@ -1,4 +1,4 @@
1
- export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, UnauthenticatedError, UnconfiguredError, UniqueViolationError, UnprocessableError, ValidationError, VersionConflictError, } from "./classes.js";
1
+ export { AccessDeniedError, ConflictError, FeatureDisabledError, IdempotentReplayError, InternalError, NotFoundError, PreconditionFailedError, RateLimitError, RateLimitUnavailableError, 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";
@@ -13,6 +13,7 @@ import { buildContentDispositionHeader } from "./content-disposition.js";
13
13
  import { createFileContext } from "./file-handle.js";
14
14
  import { fileRefEntity } from "./file-ref-entity.js";
15
15
  import { fileRefsTable } from "./file-ref-table.js";
16
+ import { resolveContentType } from "./resolve-content-type.js";
16
17
  import { buildStorageKey, parseMaxSize, resolveServedContentType, resolveUploadMimeType, validateFile, validateFileContent, } from "./types.js";
17
18
  const DEFAULT_PRIVILEGED_ROLES = ["Admin", "SystemAdmin"];
18
19
  // 15 minutes — long enough for a download to start, short enough that a
@@ -195,10 +196,14 @@ export function createFileRoutes(options) {
195
196
  if (mimeTypeResolution.kind === "rejected") {
196
197
  return c.json({ error: mimeTypeResolution.error }, 400);
197
198
  }
198
- mimeType = mimeTypeResolution.mimeType;
199
+ mimeType = resolveContentType({
200
+ declared: mimeTypeResolution.mimeType,
201
+ bytes: data,
202
+ filename: file.name,
203
+ });
199
204
  }
200
205
  const fileRefId = generateId();
201
- const storageKey = buildStorageKey(user.tenantId, entityType ?? "unattached", entityId ?? "", fieldName ?? "file", file.name, generateId(), contentValidation.kind === "normalized" ? contentValidation.extension : undefined);
206
+ const storageKey = buildStorageKey(user.tenantId, entityType ?? "unattached", entityId, fieldName ?? "file", file.name, generateId(), contentValidation.kind === "normalized" ? contentValidation.extension : undefined);
202
207
  // Write binary FIRST (outside the tx — network/disk I/O doesn't belong
203
208
  // inside a PG connection's tx window). On DB-tx rollback below the bytes
204
209
  // are orphaned in the provider; cleanup-jobs sweep those later. Losing a
@@ -10,6 +10,7 @@ export { createInMemoryFileProvider } from "./in-memory-provider.js";
10
10
  export { createLocalProvider } from "./local-provider.js";
11
11
  export type { FileProviderContext, FileProviderPlugin, FileProviderResolver, FileProviderResolverDeps, } from "./provider-resolver.js";
12
12
  export { createFileProviderForTenant, isFileProviderPlugin, makeFileProviderResolver, } from "./provider-resolver.js";
13
+ export { resolveContentType } from "./resolve-content-type.js";
13
14
  export { fileRefStorageDelta, filesStorageTrackingFeature, tenantStorageUsageTable, transferTenantStorageUsage, } from "./storage-tracking.js";
14
15
  export type { FileContentValidationResult, FileMetadata, FileStorageProvider, FileValidationOptions, SignedUrlOptions, UploadMimeTypeResolution, WriteStreamOptions, } from "./types.js";
15
16
  export { assertSafeStorageKey, buildStorageKey, normalizeMimeType, parseMaxSize, resolveUploadMimeType, sniffMimeType, tenantExportPrefix, tenantStoragePrefixes, validateFile, validateFileContent, } from "./types.js";
@@ -8,6 +8,7 @@ export { createFileRoutes, readFilesRouteOptions, resolveMaxUploadBodyBytes, } f
8
8
  export { createInMemoryFileProvider } from "./in-memory-provider.js";
9
9
  export { createLocalProvider } from "./local-provider.js";
10
10
  export { createFileProviderForTenant, isFileProviderPlugin, makeFileProviderResolver, } from "./provider-resolver.js";
11
+ export { resolveContentType } from "./resolve-content-type.js";
11
12
  export { fileRefStorageDelta, filesStorageTrackingFeature, tenantStorageUsageTable, transferTenantStorageUsage, } from "./storage-tracking.js";
12
13
  export { assertSafeStorageKey, buildStorageKey, normalizeMimeType, parseMaxSize, resolveUploadMimeType, sniffMimeType, tenantExportPrefix, tenantStoragePrefixes, validateFile, validateFileContent, } from "./types.js";
13
14
  export { createZipStream } from "./zip-stream.js";
@@ -1,3 +1,4 @@
1
+ import type { CacheSyncBus } from "@cosmicdrift/kumiko-types/cache-sync-types";
1
2
  import type { FileProviderResolver } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
2
3
  import type { DbConnection } from "../db/connection.js";
3
4
  import type { TenantDb } from "../db/tenant-db.js";
@@ -54,5 +55,6 @@ export type FileProviderResolverDeps = {
54
55
  readonly _configAccessorFactory?: ConfigAccessorFactory;
55
56
  readonly secrets?: SecretsContext;
56
57
  readonly db?: DbConnection | TenantDb;
58
+ readonly cacheSync?: CacheSyncBus;
57
59
  };
58
60
  export declare function makeFileProviderResolver(deps: FileProviderResolverDeps): FileProviderResolver;
@@ -13,6 +13,8 @@
13
13
  // (moved here from there) so existing imports keep working.
14
14
  import { EXT_FILE_PROVIDER, FILE_PROVIDER_CONFIG_KEY, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, } from "../engine/extension-names.js";
15
15
  import { SYSTEM_USER_ID } from "../engine/system-user.js";
16
+ import { SYSTEM_TENANT_ID } from "../engine/types/index.js";
17
+ import { CACHE_SYNC_TOPICS, isTenantConfigSyncMessage } from "../redis/cache-sync-topics.js";
16
18
  const FEATURE_NAME = "file-foundation";
17
19
  // extension-usage `options` is engine-payload (unknown) — structurally validate
18
20
  // instead of casting blind.
@@ -83,6 +85,16 @@ export function makeFileProviderResolver(deps) {
83
85
  // promise is evicted so a transient failure (or a secret genuinely not
84
86
  // configured yet) doesn't permanently poison this tenant's entry.
85
87
  const cache = new Map();
88
+ deps.cacheSync?.subscribe(CACHE_SYNC_TOPICS.tenantConfig, (message) => {
89
+ // skip: malformed message from another process
90
+ if (!isTenantConfigSyncMessage(message))
91
+ return;
92
+ if ("scope" in message || message.tenantId === SYSTEM_TENANT_ID)
93
+ cache.clear();
94
+ else
95
+ cache.delete(message.tenantId);
96
+ });
97
+ deps.cacheSync?.onResync(() => cache.clear());
86
98
  return (tenantId) => {
87
99
  const cached = cache.get(tenantId);
88
100
  if (cached)
@@ -99,7 +111,10 @@ export function makeFileProviderResolver(deps) {
99
111
  });
100
112
  return createFileProviderForTenant({ config, registry: deps.registry, secrets: deps.secrets, _userId: SYSTEM_USER_ID }, tenantId);
101
113
  })();
102
- built.catch(() => cache.delete(tenantId));
114
+ built.catch(() => {
115
+ if (cache.get(tenantId) === built)
116
+ cache.delete(tenantId);
117
+ });
103
118
  cache.set(tenantId, built);
104
119
  return built;
105
120
  };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Content type for a file whose sender may not have declared a useful one
3
+ * (mail attachments commonly arrive as application/octet-stream). A concrete
4
+ * declared type is returned untouched; a generic/missing one is resolved from
5
+ * the bytes first and the file extension only as fallback.
6
+ */
7
+ export declare function resolveContentType(args: {
8
+ readonly declared: string | undefined;
9
+ readonly bytes: Uint8Array;
10
+ readonly filename: string | undefined;
11
+ }): string;
@@ -0,0 +1,71 @@
1
+ import { normalizeMimeType, sniffMimeType } from "./types.js";
2
+ const OCTET_STREAM = "application/octet-stream";
3
+ const GENERIC_MIME_TYPES = new Set(["", OCTET_STREAM, "binary/octet-stream"]);
4
+ // Signatures that identify exactly one type. sniffMimeType also matches OLE and
5
+ // ZIP, which it maps to msword/docx — wrong for .xls/.xlsx/.zip, so they are
6
+ // handled separately below.
7
+ const UNAMBIGUOUS_SNIFFED_TYPES = new Set([
8
+ "image/png",
9
+ "image/jpeg",
10
+ "image/gif",
11
+ "image/webp",
12
+ "application/pdf",
13
+ ]);
14
+ const ZIP_SIGNATURE = [0x50, 0x4b, 0x03, 0x04];
15
+ const XML_PROBE_BYTES = 512;
16
+ const EXTENSION_FALLBACK = {
17
+ xml: "application/xml",
18
+ pdf: "application/pdf",
19
+ png: "image/png",
20
+ jpg: "image/jpeg",
21
+ jpeg: "image/jpeg",
22
+ gif: "image/gif",
23
+ webp: "image/webp",
24
+ zip: "application/zip",
25
+ json: "application/json",
26
+ csv: "text/csv",
27
+ txt: "text/plain",
28
+ };
29
+ function hasPrefix(bytes, prefix) {
30
+ return prefix.every((byte, i) => bytes[i] === byte);
31
+ }
32
+ // Textual start only: a NUL in the probe means binary (or UTF-16, which is
33
+ // deliberately not detected). HTML also starts with `<` and must not be
34
+ // relabelled as XML.
35
+ function looksLikeXml(bytes) {
36
+ const probe = bytes.subarray(0, XML_PROBE_BYTES);
37
+ if (probe.includes(0))
38
+ return false;
39
+ const text = new TextDecoder().decode(probe).replace(/^/, "").trimStart();
40
+ if (text.startsWith("<?xml"))
41
+ return true;
42
+ if (!/^<[A-Za-z_]/.test(text))
43
+ return false;
44
+ return !/^<(html|head|body|script)\b/i.test(text);
45
+ }
46
+ function extensionOf(filename) {
47
+ const dot = filename.lastIndexOf(".");
48
+ return dot === -1 ? "" : filename.slice(dot + 1).toLowerCase();
49
+ }
50
+ /**
51
+ * Content type for a file whose sender may not have declared a useful one
52
+ * (mail attachments commonly arrive as application/octet-stream). A concrete
53
+ * declared type is returned untouched; a generic/missing one is resolved from
54
+ * the bytes first and the file extension only as fallback.
55
+ */
56
+ export function resolveContentType(args) {
57
+ const declared = args.declared ?? "";
58
+ if (!GENERIC_MIME_TYPES.has(normalizeMimeType(declared)))
59
+ return declared;
60
+ const sniffed = sniffMimeType(args.bytes);
61
+ if (sniffed !== null && UNAMBIGUOUS_SNIFFED_TYPES.has(sniffed))
62
+ return sniffed;
63
+ if (looksLikeXml(args.bytes))
64
+ return "application/xml";
65
+ const fromExtension = EXTENSION_FALLBACK[extensionOf(args.filename ?? "")];
66
+ if (fromExtension !== undefined)
67
+ return fromExtension;
68
+ if (hasPrefix(args.bytes, ZIP_SIGNATURE))
69
+ return "application/zip";
70
+ return OCTET_STREAM;
71
+ }
@@ -47,7 +47,7 @@ export type UploadMimeTypeResolution = {
47
47
  */
48
48
  export declare function resolveUploadMimeType(fileName: string, declaredMimeType: string, content: Uint8Array): UploadMimeTypeResolution;
49
49
  export declare function assertSafeStorageKey(key: string): void;
50
- export declare function buildStorageKey(tenantId: TenantId, entityType: string, entityId: number | string, fieldName: string, fileName: string, uniqueId: string, extensionOverride?: string): string;
50
+ export declare function buildStorageKey(tenantId: TenantId, entityType: string, entityId: number | string | undefined, fieldName: string, fileName: string, uniqueId: string, extensionOverride?: string): string;
51
51
  /** Fixed leading segment so one S3 lifecycle rule (Prefix: "exports/") can expire export bundles for every tenant without matching a buildStorageKey() upload. */
52
52
  export declare function tenantExportPrefix(tenantId: TenantId): string;
53
53
  /**
@@ -261,6 +261,9 @@ export function assertSafeStorageKey(key) {
261
261
  throw new Error(`storage key contains a path-traversal segment: "${key}"`);
262
262
  }
263
263
  }
264
+ // Object stores (S3/MinIO) reject keys with an empty path segment, so an
265
+ // upload without an owning entity needs a placeholder id segment.
266
+ const NO_ENTITY_ID_SEGMENT = "none";
264
267
  export function buildStorageKey(tenantId, entityType, entityId, fieldName, fileName, uniqueId,
265
268
  // Set when validateFileContent normalized the upload to a different real
266
269
  // type — the storage key then reflects the sniffed type, not the user's
@@ -268,7 +271,8 @@ export function buildStorageKey(tenantId, entityType, entityId, fieldName, fileN
268
271
  extensionOverride) {
269
272
  const rawExt = extensionOverride ?? fileName.split(".").pop() ?? "";
270
273
  const ext = /^[A-Za-z0-9]+$/.test(rawExt) ? rawExt.toLowerCase() : "bin";
271
- return `${tenantId}/${entityType}/${entityId}/${fieldName}/${uniqueId}.${ext}`;
274
+ const entitySegment = entityId === undefined || entityId === "" ? NO_ENTITY_ID_SEGMENT : entityId;
275
+ return `${tenantId}/${entityType}/${entitySegment}/${fieldName}/${uniqueId}.${ext}`;
272
276
  }
273
277
  const EXPORTS_PREFIX_SEGMENT = "exports";
274
278
  /** Fixed leading segment so one S3 lifecycle rule (Prefix: "exports/") can expire export bundles for every tenant without matching a buildStorageKey() upload. */
@@ -11,7 +11,7 @@ import { InternalError } from "../errors/index.js";
11
11
  import { isKumikoError } from "../errors/kumiko-error.js";
12
12
  import { createFileContext } from "../files/file-handle.js";
13
13
  import { createFallbackLogger } from "../logging/index.js";
14
- import { createEscapeHatchReporter, emitJobLastSuccess, emitJobQueueDepth, getFallbackTracer, registerStandardMetrics, } from "../observability/index.js";
14
+ import { createEscapeHatchProcessDedup, createEscapeHatchReporter, emitJobLastSuccess, emitJobQueueDepth, getFallbackTracer, registerStandardMetrics, } from "../observability/index.js";
15
15
  import { createDistributedLock } from "../pipeline/distributed-lock.js";
16
16
  import { RedisKeys } from "../pipeline/redis-keys.js";
17
17
  import { buildPersonalDataGate, isPersonalDataGated, parseWriteOrigin, } from "../pipeline/write-origin.js";
@@ -316,6 +316,8 @@ export function createJobRunner(options) {
316
316
  // must not depend on buildServer having done it first. Idempotent.
317
317
  if (context.meter)
318
318
  registerStandardMetrics(context.meter);
319
+ // Instance-scoped, not module-level: each runner (and each test) audits a system cron's declared hatch once.
320
+ const systemCronEscapeHatchDedup = createEscapeHatchProcessDedup(["unsafe-raw", "global-write"]);
319
321
  // Set at the top of stop() — a graceful shutdown closes the redis/BullMQ
320
322
  // clients itself, which fires the exact same 'error' listeners below with
321
323
  // an expected "Connection is closed." Downgrading to debug once stopping
@@ -677,7 +679,7 @@ export function createJobRunner(options) {
677
679
  // must resolve for jobs the same way they do for write-handlers, or an
678
680
  // app-author job calling ctx.notify(...)/ctx.config(...) hits a TypeError
679
681
  // at runtime the write-handler path never would (framework#1532).
680
- const notify = context._notifyFactory?.(jobSystemUser, tenantId);
682
+ const notify = context._notifyFactory?.(jobSystemUser, tenantId, selfRunner);
681
683
  const configDb = context.db; // @cast-boundary db-operator
682
684
  // Shared by the config accessor and ctx.derivatives below — both need the
683
685
  // same tenant-scoped db, and building it twice would let the two calls
@@ -692,12 +694,15 @@ export function createJobRunner(options) {
692
694
  : undefined;
693
695
  const isSystemJob = registry.isJobSystemScoped(jobName);
694
696
  // One reporter for ctx.systemDb and ctx.db.unsafeRaw() so both dedupe in the same window.
697
+ const isSystemCronRun = triggeredById === null && jobDef.escapeHatch !== undefined && "cron" in jobDef.trigger;
695
698
  const reportEscapeHatch = createEscapeHatchReporter({
696
699
  handler: jobName,
697
700
  tenantId,
698
701
  actor: jobSystemUser.id,
699
702
  sink: context._escapeHatchAuditSink,
700
703
  log: context.log,
704
+ ...(isSystemCronRun && { processDedup: systemCronEscapeHatchDedup }),
705
+ ...(context.meter && { meter: context.meter }),
701
706
  });
702
707
  const systemDb = isSystemJob && tenantScopedDb
703
708
  ? createUncheckedSystemDb(tenantScopedDb, undefined, reportEscapeHatch)
@@ -705,6 +710,7 @@ export function createJobRunner(options) {
705
710
  const jobDb = configDb
706
711
  ? createTenantDb(configDb, tenantId, "tenant", context.tracer, context.meter, undefined, {
707
712
  unsafeRaw: escapeHatchFor(jobDef.escapeHatch, "unsafeRaw"),
713
+ globalWrites: escapeHatchFor(jobDef.escapeHatch, "globalWrites"),
708
714
  report: reportEscapeHatch,
709
715
  ...(jobPersonalDataGate && { personalDataGate: jobPersonalDataGate }),
710
716
  })
@@ -1,5 +1,6 @@
1
- import type { EscapeHatchAuditSink, EscapeHatchReporter, EscapeHatchUseEvent, TenantId } from "../engine/types/index.js";
1
+ import type { EscapeHatchAuditSink, EscapeHatchKind, EscapeHatchReporter, EscapeHatchUseEvent, TenantId } from "../engine/types/index.js";
2
2
  import type { Logger } from "../logging/types.js";
3
+ import type { Meter } from "./types/index.js";
3
4
  export declare const ESCAPE_HATCH_USED_SIGNAL = "security:escape-hatch-used";
4
5
  export type EscapeHatchReportWindow = {
5
6
  shouldReport(key: string, nowMs: number): boolean;
@@ -8,6 +9,11 @@ export declare function createEscapeHatchReportWindow(opts?: {
8
9
  readonly windowMs?: number;
9
10
  readonly maxEntries?: number;
10
11
  }): EscapeHatchReportWindow;
12
+ export type EscapeHatchProcessDedup = {
13
+ readonly kinds: ReadonlySet<EscapeHatchKind>;
14
+ readonly seen: Set<string>;
15
+ };
16
+ export declare function createEscapeHatchProcessDedup(kinds: readonly EscapeHatchKind[]): EscapeHatchProcessDedup;
11
17
  export declare function reportEscapeHatchUse(event: EscapeHatchUseEvent, deps: {
12
18
  readonly sink?: EscapeHatchAuditSink;
13
19
  readonly log?: Logger;
@@ -19,6 +25,9 @@ export declare function createEscapeHatchReporter(opts: {
19
25
  readonly sink?: EscapeHatchAuditSink;
20
26
  readonly log?: Logger;
21
27
  readonly window?: EscapeHatchReportWindow;
28
+ readonly processDedup?: EscapeHatchProcessDedup;
29
+ readonly meter?: Meter;
30
+ readonly now?: () => number;
22
31
  }): EscapeHatchReporter;
23
32
  export declare const UNATTRIBUTED_ACTOR = "system";
24
33
  export declare function fallbackEscapeHatchReporter(tenantId: TenantId): EscapeHatchReporter;
@@ -1,3 +1,4 @@
1
+ import { emitEscapeHatchUse } from "./standard-metrics.js";
1
2
  export const ESCAPE_HATCH_USED_SIGNAL = "security:escape-hatch-used";
2
3
  const DEFAULT_WINDOW_MS = 60_000;
3
4
  const DEFAULT_MAX_ENTRIES = 1_000;
@@ -24,6 +25,9 @@ export function createEscapeHatchReportWindow(opts) {
24
25
  },
25
26
  };
26
27
  }
28
+ export function createEscapeHatchProcessDedup(kinds) {
29
+ return { kinds: new Set(kinds), seen: new Set() };
30
+ }
27
31
  // Default window for createEscapeHatchReporter callers that don't pass their own.
28
32
  const fallbackReportWindow = createEscapeHatchReportWindow();
29
33
  const consoleLogger = {
@@ -67,7 +71,21 @@ export function reportEscapeHatchUse(event, deps) {
67
71
  }
68
72
  export function createEscapeHatchReporter(opts) {
69
73
  const window = opts.window ?? fallbackReportWindow;
74
+ const now = opts.now ?? Date.now;
70
75
  return (kind, reason, target) => {
76
+ if (opts.meter)
77
+ emitEscapeHatchUse(opts.meter, opts.handler, kind);
78
+ const { processDedup } = opts;
79
+ if (processDedup?.kinds.has(kind)) {
80
+ const processKey = JSON.stringify([opts.handler, kind, opts.tenantId]);
81
+ // skip: this (handler, kind, tenant) was already audited by this process
82
+ if (processDedup.seen.has(processKey))
83
+ return;
84
+ processDedup.seen.add(processKey);
85
+ reportEscapeHatchUse({ handler: opts.handler, kind, reason, tenantId: opts.tenantId, actor: opts.actor, target }, { sink: opts.sink, log: opts.log });
86
+ // skip: audited above, the window dedup below only serves the other kinds
87
+ return;
88
+ }
71
89
  const key = JSON.stringify([
72
90
  opts.handler,
73
91
  kind,
@@ -78,7 +96,7 @@ export function createEscapeHatchReporter(opts) {
78
96
  target?.tenantId ?? null,
79
97
  ]);
80
98
  // skip: same (handler, kind, reason, target) already reported within the window — dedup
81
- if (!window.shouldReport(key, Date.now()))
99
+ if (!window.shouldReport(key, now()))
82
100
  return;
83
101
  reportEscapeHatchUse({ handler: opts.handler, kind, reason, tenantId: opts.tenantId, actor: opts.actor, target }, { sink: opts.sink, log: opts.log });
84
102
  };
@@ -1,6 +1,6 @@
1
1
  export { type ConsoleProviderOptions, createConsoleProvider } from "./console-provider.js";
2
2
  export { observabilityContext } from "./context.js";
3
- export { createEscapeHatchReporter, createEscapeHatchReportWindow, ESCAPE_HATCH_USED_SIGNAL, type EscapeHatchReportWindow, fallbackEscapeHatchReporter, reportEscapeHatchUse, UNATTRIBUTED_ACTOR, } from "./escape-hatch-report.js";
3
+ export { createEscapeHatchProcessDedup, createEscapeHatchReporter, createEscapeHatchReportWindow, ESCAPE_HATCH_USED_SIGNAL, type EscapeHatchProcessDedup, type EscapeHatchReportWindow, fallbackEscapeHatchReporter, reportEscapeHatchUse, UNATTRIBUTED_ACTOR, } from "./escape-hatch-report.js";
4
4
  export { getFallbackMeter, getFallbackProvider, getFallbackTracer } from "./fallback.js";
5
5
  export { generateSpanId, generateTraceId } from "./ids.js";
6
6
  export { buildMetricName, validateLabelKey, validateMetricName, } from "./metric-validator.js";
@@ -12,5 +12,5 @@ export { type MetricEvent, type MetricEventHandler, RecordingMeter, } from "./re
12
12
  export { type RecordedSpan, RecordingTracer, type RecordingTracerOptions, serializeSpanContext, } from "./recording-tracer.js";
13
13
  export { wrapRedisClient } from "./redis-wrapper.js";
14
14
  export { DEFAULT_SENSITIVE_CONFIG, mergeSensitiveConfig, REDACTED, redactAttributes, redactHeaders, redactQueryString, redactValue, shouldRedactAttribute, } from "./sensitive-filter.js";
15
- export { emitDbQuery, emitDispatcherError, emitDispatcherHandler, emitEventConsumerLag, emitEventConsumerPassOutcome, emitEventConsumerPassSkipped, emitEventConsumerRearmExhausted, emitEventDispatcherListenConnected, emitHttpRequest, emitJobLastSuccess, emitJobQueueDepth, registerStandardMetrics, STANDARD_METRIC_DEFS, } from "./standard-metrics.js";
15
+ export { emitDbQuery, emitDispatcherError, emitDispatcherHandler, emitEscapeHatchUse, emitEventConsumerLag, emitEventConsumerPassOutcome, emitEventConsumerPassSkipped, emitEventConsumerRearmExhausted, emitEventDispatcherListenConnected, emitHttpRequest, emitJobLastSuccess, emitJobQueueDepth, registerStandardMetrics, STANDARD_METRIC_DEFS, } from "./standard-metrics.js";
16
16
  export type { Counter, Gauge, Histogram, Meter, MetricDefinition, MetricLabels, MetricsHandle, MetricType, ObservabilityOptions, ObservabilityProvider, SamplingConfig, SensitiveFilterConfig, SerializedTraceContext, Span, SpanAttributes, SpanAttributeValue, SpanKind, SpanStatus, StartSpanOptions, Tracer, } from "./types/index.js";
@@ -1,7 +1,7 @@
1
1
  // Public surface of the observability module.
2
2
  export { createConsoleProvider } from "./console-provider.js";
3
3
  export { observabilityContext } from "./context.js";
4
- export { createEscapeHatchReporter, createEscapeHatchReportWindow, ESCAPE_HATCH_USED_SIGNAL, fallbackEscapeHatchReporter, reportEscapeHatchUse, UNATTRIBUTED_ACTOR, } from "./escape-hatch-report.js";
4
+ export { createEscapeHatchProcessDedup, createEscapeHatchReporter, createEscapeHatchReportWindow, ESCAPE_HATCH_USED_SIGNAL, fallbackEscapeHatchReporter, reportEscapeHatchUse, UNATTRIBUTED_ACTOR, } from "./escape-hatch-report.js";
5
5
  export { getFallbackMeter, getFallbackProvider, getFallbackTracer } from "./fallback.js";
6
6
  export { generateSpanId, generateTraceId } from "./ids.js";
7
7
  export { buildMetricName, validateLabelKey, validateMetricName, } from "./metric-validator.js";
@@ -13,4 +13,4 @@ export { RecordingMeter, } from "./recording-meter.js";
13
13
  export { RecordingTracer, serializeSpanContext, } from "./recording-tracer.js";
14
14
  export { wrapRedisClient } from "./redis-wrapper.js";
15
15
  export { DEFAULT_SENSITIVE_CONFIG, mergeSensitiveConfig, REDACTED, redactAttributes, redactHeaders, redactQueryString, redactValue, shouldRedactAttribute, } from "./sensitive-filter.js";
16
- export { emitDbQuery, emitDispatcherError, emitDispatcherHandler, emitEventConsumerLag, emitEventConsumerPassOutcome, emitEventConsumerPassSkipped, emitEventConsumerRearmExhausted, emitEventDispatcherListenConnected, emitHttpRequest, emitJobLastSuccess, emitJobQueueDepth, registerStandardMetrics, STANDARD_METRIC_DEFS, } from "./standard-metrics.js";
16
+ export { emitDbQuery, emitDispatcherError, emitDispatcherHandler, emitEscapeHatchUse, emitEventConsumerLag, emitEventConsumerPassOutcome, emitEventConsumerPassSkipped, emitEventConsumerRearmExhausted, emitEventDispatcherListenConnected, emitHttpRequest, emitJobLastSuccess, emitJobQueueDepth, registerStandardMetrics, STANDARD_METRIC_DEFS, } from "./standard-metrics.js";
@@ -43,3 +43,4 @@ export declare function emitEventConsumerPassOutcome(meter: Meter, labels: {
43
43
  }, processed: number, failed: number): void;
44
44
  export declare function emitJobQueueDepth(meter: Meter, lane: string, counts: Readonly<Record<string, number>>): void;
45
45
  export declare function emitJobLastSuccess(meter: Meter, job: string): void;
46
+ export declare function emitEscapeHatchUse(meter: Meter, handler: string, kind: string): void;
@@ -140,6 +140,15 @@ export const STANDARD_METRIC_DEFS = [
140
140
  description: "Unix timestamp of the last successful run, per registered job.",
141
141
  labels: ["job"],
142
142
  },
143
+ // Every escape-hatch use counts here, including the ones the audit sink
144
+ // dedups away: a system cron firing every minute writes one audit event per
145
+ // process, so this counter is the only record of how often it ran.
146
+ {
147
+ name: "kumiko_escape_hatch_uses_total",
148
+ type: "counter",
149
+ description: "Escape-hatch uses by handler and kind, whether or not an audit event was written.",
150
+ labels: ["handler", "kind"],
151
+ },
143
152
  ];
144
153
  export function registerStandardMetrics(meter) {
145
154
  for (const def of STANDARD_METRIC_DEFS) {
@@ -251,3 +260,8 @@ export function emitJobQueueDepth(meter, lane, counts) {
251
260
  export function emitJobLastSuccess(meter, job) {
252
261
  meter.gauge("kumiko_job_last_success_timestamp_seconds").set(Date.now() / 1000, { job });
253
262
  }
263
+ // `handler` is a registry-declared handler/job name, `kind` a closed union —
264
+ // no tenant or user input reaches the label set.
265
+ export function emitEscapeHatchUse(meter, handler, kind) {
266
+ meter.counter("kumiko_escape_hatch_uses_total").inc(1, { handler, kind });
267
+ }
@@ -1,10 +1,11 @@
1
+ import type { CacheSyncBus } from "@cosmicdrift/kumiko-types/cache-sync-types";
1
2
  import type { SseBroker } from "../api/sse-broker.js";
2
3
  import type { DbConnection, DbTx } from "../db/connection.js";
3
4
  import type { buildEntityTable } from "../db/table-builder.js";
4
5
  import type { defineTransitions } from "../engine/state-machine.js";
5
6
  import type { EffectiveFeaturesResolver } from "../engine/tier-resolver-extension.js";
6
7
  import type { TenantId } from "../engine/types/identifiers.js";
7
- import type { AppContext, HandlerContext, JobRunnerRef, RateLimitDeclaration, Registry, SessionUser, WriteResult } from "../engine/types/index.js";
8
+ import type { AppContext, HandlerContext, JobRunnerRef, PayloadRateLimitOption, RateLimitDeclaration, Registry, SessionUser, WriteResult } from "../engine/types/index.js";
8
9
  import { type WriteErrorInfo } from "../errors/index.js";
9
10
  import { type EscapeHatchReportWindow, type getFallbackMeter, getFallbackTracer } from "../observability/index.js";
10
11
  import { type AfterCommitHook } from "./dispatcher-utils.js";
@@ -40,6 +41,7 @@ export type DispatchContext = {
40
41
  tableCache: Map<string, ReturnType<typeof buildEntityTable>>;
41
42
  transitionCache: Map<string, ReturnType<typeof defineTransitions>>;
42
43
  tenantTimezoneCache: TenantTimezoneCache;
44
+ cacheSync: CacheSyncBus | undefined;
43
45
  escapeHatchReportWindow: EscapeHatchReportWindow;
44
46
  tracer: ReturnType<typeof getFallbackTracer>;
45
47
  meter: ReturnType<typeof getFallbackMeter>;
@@ -54,4 +56,5 @@ export declare function runStreamInstrumented<T>(ctx: DispatchContext, type: str
54
56
  export declare function checkFeatureEnabled(ctx: DispatchContext, qualifiedHandler: string, tenantId: TenantId): Promise<import("../errors/index.js").FeatureDisabledError | undefined>;
55
57
  export declare function ensureFeatureEnabled(ctx: DispatchContext, qualifiedHandler: string, tenantId: TenantId): Promise<void>;
56
58
  export declare function enforceRateLimit(ctx: DispatchContext, rateLimit: RateLimitDeclaration | undefined, handlerName: string, user: SessionUser, isSystemScope: boolean): Promise<void>;
59
+ export declare function enforcePayloadRateLimits(ctx: DispatchContext, limits: readonly PayloadRateLimitOption[] | undefined, handlerName: string, user: SessionUser, data: unknown): Promise<void>;
57
60
  export declare function resolveAuthClaimsFn(ctx: DispatchContext, user: SessionUser): Promise<Record<string, unknown>>;