@cosmicdrift/kumiko-framework 0.374.0 → 0.377.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 (65) hide show
  1. package/AGENTS.md +2 -2
  2. package/agents/examples/apex-landing.md +1 -1
  3. package/agents/examples/apex-surface-auth.md +3 -3
  4. package/agents/examples/compose-stacks.md +15 -8
  5. package/agents/examples/embedded-entity-form.md +1 -1
  6. package/agents/examples/notes-history-basic.md +1 -1
  7. package/agents/examples/tags-basic.md +1 -1
  8. package/agents/examples/user-profile.md +1 -1
  9. package/agents/examples/wizard-form.md +4 -4
  10. package/agents/widgets.md +3 -3
  11. package/dist/api/index.d.ts +1 -1
  12. package/dist/api/index.js +1 -1
  13. package/dist/api/routes.js +2 -2
  14. package/dist/api/server.js +2 -1
  15. package/dist/config/index.d.ts +4 -0
  16. package/dist/db/index.d.ts +2 -2
  17. package/dist/db/index.js +1 -1
  18. package/dist/db/migrate-runner.js +12 -4
  19. package/dist/db/money.js +1 -1
  20. package/dist/db/query.js +14 -14
  21. package/dist/db/replay-migration-sql.d.ts +1 -0
  22. package/dist/db/replay-migration-sql.js +115 -11
  23. package/dist/db/tenant-db.d.ts +7 -0
  24. package/dist/db/tenant-db.js +17 -5
  25. package/dist/engine/boot-validator/screens.js +7 -1
  26. package/dist/engine/index.d.ts +2 -1
  27. package/dist/engine/index.js +1 -0
  28. package/dist/engine/parse-ref-target.js +1 -1
  29. package/dist/engine/provider-plugin-context.d.ts +3 -0
  30. package/dist/engine/provider-plugin-context.js +28 -0
  31. package/dist/engine/schema-builder.js +7 -0
  32. package/dist/engine/types/index.d.ts +1 -1
  33. package/dist/env/index.js +1 -1
  34. package/dist/errors/zod-bridge.js +1 -1
  35. package/dist/files/file-ref-entity.d.ts +6 -6
  36. package/dist/files/file-ref-table.d.ts +6 -6
  37. package/dist/files/index.d.ts +1 -1
  38. package/dist/files/index.js +1 -1
  39. package/dist/files/provider-resolver.d.ts +17 -17
  40. package/dist/files/provider-resolver.js +1 -1
  41. package/dist/http/egress.d.ts +14 -0
  42. package/dist/http/egress.js +144 -0
  43. package/dist/http/index.d.ts +3 -2
  44. package/dist/http/index.js +2 -1
  45. package/dist/http/policy.d.ts +30 -0
  46. package/dist/http/policy.js +299 -0
  47. package/dist/i18n/required-surface-keys.js +2 -0
  48. package/dist/jobs/job-runner.js +4 -2
  49. package/dist/pipeline/config-accessor-factory.d.ts +3 -0
  50. package/dist/pipeline/config-accessor-factory.js +3 -0
  51. package/dist/pipeline/dispatch-shared.js +7 -5
  52. package/dist/pipeline/index.d.ts +1 -0
  53. package/dist/pipeline/index.js +1 -0
  54. package/dist/ui-types/index.d.ts +1 -1
  55. package/dist/upgrade-changelog.d.ts +25 -0
  56. package/dist/upgrade-changelog.js +48 -0
  57. package/dist/upgrade-cli.js +78 -13
  58. package/package.json +3 -5
  59. package/presets/biome-app.json +8 -0
  60. package/src/changes.json +71 -0
  61. package/src/scripts/codemod/README.md +25 -0
  62. package/src/scripts/codemod/codemod-support.ts +64 -6
  63. package/src/scripts/codemod/package-merge.ts +260 -0
  64. package/src/scripts/codemod/s3-imports.ts +4 -42
  65. package/src/scripts/codemod/testing-injection.ts +135 -0
@@ -36,6 +36,13 @@ export declare function acknowledgeConventionCrossTenant(tenantDb: TenantDb, rea
36
36
  readonly deferReport?: boolean;
37
37
  }): TenantDb;
38
38
  export declare function reportConventionCrossTenantUse(acknowledged: TenantDb, target?: EscapeHatchTarget): void;
39
+ export type OutsideTransactionContext = {
40
+ readonly dbOutsideTransaction: TenantDb | undefined;
41
+ };
42
+ export declare function hasOutsideTransactionDb(ctx: OutsideTransactionContext): ctx is OutsideTransactionContext & {
43
+ readonly dbOutsideTransaction: TenantDb;
44
+ };
45
+ export declare function requireOutsideTransactionDb(ctx: OutsideTransactionContext): TenantDb;
39
46
  export declare function runInOwnTransaction<T>(tenantDb: TenantDb, fn: (txDb: TenantDb) => Promise<T>): Promise<T>;
40
47
  export type CreateTenantDbOptions = {
41
48
  readonly db: DbRunner;
@@ -103,7 +103,7 @@ function buildUncheckedSystemDb(db, dbOutsideTransaction, report, gate) {
103
103
  // AccessDeniedError) because this is a dispatch wiring fault, not a
104
104
  // tenant-access denial — mirrors the "no database connection configured"
105
105
  // case in dispatch-shared.ts's appendDomainEvent.
106
- function requireOutsideTransactionDb() {
106
+ function requireSystemOutsideTransactionDb() {
107
107
  if (!dbOutsideTransaction) {
108
108
  throw new InternalError({
109
109
  message: "systemScope() outsideTransaction check failed: no outside-transaction database " +
@@ -173,13 +173,13 @@ function buildUncheckedSystemDb(db, dbOutsideTransaction, report, gate) {
173
173
  message: `systemScope() outsideTransaction tenant self-check failed: expected "${db.tenantId}", got "${tenantId}"`,
174
174
  });
175
175
  }
176
- return requireOutsideTransactionDb();
176
+ return requireSystemOutsideTransactionDb();
177
177
  },
178
178
  acknowledgeCrossTenant(reason) {
179
179
  if (reason.trim().length === 0) {
180
180
  throw new Error("acknowledgeCrossTenant requires a non-empty reason");
181
181
  }
182
- const result = requireOutsideTransactionDb();
182
+ const result = requireSystemOutsideTransactionDb();
183
183
  report("acknowledge-cross-tenant", reason);
184
184
  return result;
185
185
  },
@@ -301,12 +301,24 @@ export function reportConventionCrossTenantUse(acknowledged, target) {
301
301
  reporter(target);
302
302
  }
303
303
  const ownTransactionRebinders = new WeakMap();
304
+ export function hasOutsideTransactionDb(ctx) {
305
+ return ctx.dbOutsideTransaction !== undefined;
306
+ }
307
+ export function requireOutsideTransactionDb(ctx) {
308
+ if (!hasOutsideTransactionDb(ctx)) {
309
+ throw new InternalError({
310
+ message: "ctx.dbOutsideTransaction is not available in this dispatch phase (member-resolution, before-transaction denial) or metering tenant mode.",
311
+ });
312
+ }
313
+ return ctx.dbOutsideTransaction;
314
+ }
304
315
  export async function runInOwnTransaction(tenantDb, fn) {
305
316
  const rebind = ownTransactionRebinders.get(tenantDb);
306
317
  if (!rebind) {
307
318
  throw new InternalError({
308
- message: "runInOwnTransaction received a TenantDb not built by createTenantDb — " +
309
- "no transaction rebinder bound.",
319
+ message: "runInOwnTransaction received a TenantDb without a transaction rebinder: either it was not " +
320
+ "built by createTenantDb, or it is the fail-closed ctx.db/ctx.dbOutsideTransaction of an " +
321
+ "r.systemScope() handler (use ctx.systemDb.outsideTransaction there).",
310
322
  });
311
323
  }
312
324
  return rebind(fn);
@@ -319,7 +319,7 @@ function validateWizardLayout(featureName, screenId, screenType, layout, feature
319
319
  // typed IconKey, a closed union — but that only protects call sites this
320
320
  // repo's own `tsc --build` actually covers (samples and external consumers
321
321
  // aren't in that project graph). The renderer's actionIconFor
322
- // (renderer-web/src/primitives/index.tsx) already falls back to "no icon"
322
+ // (renderer/src/web/primitives/index.tsx) already falls back to "no icon"
323
323
  // for an unregistered key instead of crashing — this check makes the same
324
324
  // gap fail loudly at boot instead of silently at render.
325
325
  function isRegisteredIcon(icon) {
@@ -909,6 +909,12 @@ function validateInlineFormNavTargets(feature, screenId, screen, kind, lookups)
909
909
  if (screen.redirect !== undefined) {
910
910
  validateRedirectTarget(feature, screenId, kind, screen.redirect, lookups);
911
911
  }
912
+ if (screen.type === "actionForm" &&
913
+ screen.successFacts !== undefined &&
914
+ screen.redirect !== undefined) {
915
+ throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (actionForm) sets both redirect and successFacts — ` +
916
+ `redirect leaves the screen, successFacts are shown on it. Use one of them.`);
917
+ }
912
918
  if (screen.type === "actionForm" && screen.successLinks !== undefined) {
913
919
  if (screen.redirect !== undefined) {
914
920
  throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (actionForm) sets both redirect and successLinks — ` +
@@ -57,6 +57,7 @@ export { parseRefTarget, parseRefTargetEntityName } from "./parse-ref-target.ts"
57
57
  export { buildPipelineSteps, stepsPipeline } from "./pipeline.ts";
58
58
  export { projectAppSchemaForRoles } from "./project-app-schema-for-roles.ts";
59
59
  export { defineApply, defineMspApply, setFields } from "./projection-helpers.ts";
60
+ export { providerPluginContextFromHandler, providerPluginContextFromJob, } from "./provider-plugin-context.ts";
60
61
  export type { BuiltinQnType, ParsedQn, QnType } from "./qualified-name.ts";
61
62
  export { isValidQn, parseQn, QnTypes, qn, toKebab } from "./qualified-name.ts";
62
63
  export { readClaim } from "./read-claim.ts";
@@ -84,7 +85,7 @@ export { assertNotificationVariables, templateVariable, templateVariablesToZod,
84
85
  export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, requireTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.ts";
85
86
  export { findOverDeepTransferChain, resolveTransferAdjacency as resolveEntityTransferAdjacency, type TransferAdjacency, type TransferEdge, type TransferEdgeLink, } from "./transfer-adjacency.ts";
86
87
  export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.ts";
87
- export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, BootOptionName, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCurrencyFormat, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardProgressTone, DashboardRecordBinding, DashboardRefresh, DashboardRelativeTimeFormat, DashboardRelativeTimeParam, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextBlock, DashboardTextPanel, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityObjectDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookDefinition, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MaxPerTenantOptions, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreparedWriteEvent, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StackBootRequirement, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.ts";
88
+ export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, BootOptionName, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCurrencyFormat, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardProgressTone, DashboardRecordBinding, DashboardRefresh, DashboardRelativeTimeFormat, DashboardRelativeTimeParam, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextBlock, DashboardTextPanel, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityObjectDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookDefinition, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MaxPerTenantOptions, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreparedWriteEvent, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, ProviderPluginContext, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StackBootRequirement, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.ts";
88
89
  export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.ts";
89
90
  export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.ts";
90
91
  export { runValidation } from "./validation.ts";
@@ -56,6 +56,7 @@ export { parseRefTarget, parseRefTargetEntityName } from "./parse-ref-target.js"
56
56
  export { buildPipelineSteps, stepsPipeline } from "./pipeline.js";
57
57
  export { projectAppSchemaForRoles } from "./project-app-schema-for-roles.js";
58
58
  export { defineApply, defineMspApply, setFields } from "./projection-helpers.js";
59
+ export { providerPluginContextFromHandler, providerPluginContextFromJob, } from "./provider-plugin-context.js";
59
60
  export { isValidQn, parseQn, QnTypes, qn, toKebab } from "./qualified-name.js";
60
61
  export { readClaim } from "./read-claim.js";
61
62
  export { createRegistry } from "./registry.js";
@@ -5,7 +5,7 @@
5
5
  // - "users:user" → cross-feature ref (qualifiziert)
6
6
  //
7
7
  // Lebt im framework-Package damit Server-Validator + Renderer (über
8
- // Re-Export aus @cosmicdrift/kumiko-headless) denselben Parser nutzen — die
8
+ // Re-Export aus @cosmicdrift/kumiko-renderer/headless) denselben Parser nutzen — die
9
9
  // Convention darf nicht zweimal implementiert werden.
10
10
  export function parseRefTarget(raw, currentFeature) {
11
11
  const idx = raw.indexOf(":");
@@ -0,0 +1,3 @@
1
+ import type { HandlerContext, JobContext, ProviderPluginContext } from "@cosmicdrift/kumiko-types/handlers";
2
+ export declare function providerPluginContextFromHandler(ctx: HandlerContext): ProviderPluginContext;
3
+ export declare function providerPluginContextFromJob(ctx: JobContext): ProviderPluginContext;
@@ -0,0 +1,28 @@
1
+ export function providerPluginContextFromHandler(ctx) {
2
+ return {
3
+ db: ctx.db,
4
+ dbOutsideTransaction: ctx.dbOutsideTransaction,
5
+ systemDb: ctx.systemDb,
6
+ registry: ctx.registry,
7
+ user: ctx.user,
8
+ log: ctx.log,
9
+ triggeredBy: ctx.triggeredBy,
10
+ config: ctx.config,
11
+ secrets: ctx.secrets,
12
+ };
13
+ }
14
+ // Jobs have no request user: the job's system user is the acting identity, and no
15
+ // outside-transaction handle exists because jobs never run inside the handler's tx.
16
+ export function providerPluginContextFromJob(ctx) {
17
+ return {
18
+ db: ctx.db,
19
+ dbOutsideTransaction: undefined,
20
+ systemDb: ctx.systemDb,
21
+ registry: ctx.registry,
22
+ user: ctx.systemUser,
23
+ log: ctx.log,
24
+ triggeredBy: ctx.triggeredBy,
25
+ config: ctx.config,
26
+ secrets: ctx.secrets,
27
+ };
28
+ }
@@ -4,6 +4,7 @@ import { isValidIanaTimeZone } from "../time/index.js";
4
4
  import { assertUnreachable } from "../utils/index.js";
5
5
  import { withDerivedCells } from "./embedded-derived.js";
6
6
  import { DEFAULT_CURRENCIES } from "./types/index.js";
7
+ const MONTH_PATTERN = /^\d{4}-(0[1-9]|1[0-2])$/;
7
8
  // True if `n` carries at most `scale` decimal places. A relative epsilon
8
9
  // tolerates float artifacts (`0.1 + 0.2 = 0.30000000000000004` is accepted at
9
10
  // scale 2) — the exact `toFixed`-roundtrip-equality it replaces rejected such
@@ -106,6 +107,12 @@ export function fieldToZod(field, currencies, opts = {}) {
106
107
  schema = schema.email();
107
108
  if (field.format === "url")
108
109
  schema = schema.url();
110
+ if (field.format === "month") {
111
+ // "" stays valid for an optional field; `required` rejects it via min(1) below.
112
+ schema = schema.refine((value) => value === "" || MONTH_PATTERN.test(value), {
113
+ message: "expected a month as YYYY-MM",
114
+ });
115
+ }
109
116
  if (field.required)
110
117
  schema = schema.min(1);
111
118
  return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
@@ -7,7 +7,7 @@ export type { ExtensionOptionsArgs, ExtensionOptionsFor, KumikoExtensionOptionsM
7
7
  export type { BootCheckContext, BootCheckFn, BootOptionName, EntityObjectDefinition, FeatureDefinition, FeatureMetricDef, FeatureMetricType, FeatureRegistrar, HookDefinition, MetricOptions, Registry, SecretKeyDefinition, SecretKeyHandle, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretOptions, StackBootRequirement, 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, EntityEventMigration, 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, BeforeTransactionPhase, BeforeTransactionResult, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadBucketNormalization, PayloadRateLimitOption, PreparedWriteEvent, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitOptions, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TenantWriteTarget, 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, BeforeTransactionPhase, BeforeTransactionResult, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadBucketNormalization, PayloadRateLimitOption, PreparedWriteEvent, ProviderPluginContext, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitOptions, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TenantWriteTarget, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
11
11
  export { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, declaredRateLimitOptions, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, isOpenToAllGranted, isRateLimitDisabled, isRateLimitOption, 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";
package/dist/env/index.js CHANGED
@@ -277,7 +277,7 @@ function formatBootError(errors) {
277
277
  }
278
278
  }
279
279
  lines.push("");
280
- lines.push("See: kumiko-platform/docs/runbooks/standard-deploy-app.md#step-1-boot-dry-run-lokal");
280
+ lines.push("See: https://github.com/CosmicDriftGameStudio/infra/blob/main/docs/runbooks/standard-deploy-app.md#schritt-1--env-vertrag-lokal-ausgeben");
281
281
  return lines.join("\n");
282
282
  }
283
283
  export class KumikoBootError extends Error {
@@ -35,7 +35,7 @@ export function validationErrorFromZod(error) {
35
35
  // `code: "custom"` is zod's catch-all for every superRefine/refine check;
36
36
  // left mechanical it'd collapse onto one generic key, so a superRefine can
37
37
  // set `params.i18nKey` to override it. Keep in sync with the client mirror
38
- // (packages/headless/src/form/zod-bridge.ts).
38
+ // (packages/renderer/src/headless/form/zod-bridge.ts).
39
39
  function resolveI18nKey(issue) {
40
40
  if (issue.code === "custom") {
41
41
  const override = issue.params?.["i18nKey"];
@@ -14,7 +14,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
14
14
  readonly sensitive?: boolean;
15
15
  readonly writeOnly?: boolean;
16
16
  readonly readAsInstruction?: boolean;
17
- readonly format?: "email" | "url" | "phone" | "password";
17
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
18
18
  readonly default?: string;
19
19
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
20
20
  readonly multiline?: boolean | {
@@ -37,7 +37,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
37
37
  readonly sensitive?: boolean;
38
38
  readonly writeOnly?: boolean;
39
39
  readonly readAsInstruction?: boolean;
40
- readonly format?: "email" | "url" | "phone" | "password";
40
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
41
41
  readonly default?: string;
42
42
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
43
43
  readonly multiline?: boolean | {
@@ -60,7 +60,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
60
60
  readonly sensitive?: boolean;
61
61
  readonly writeOnly?: boolean;
62
62
  readonly readAsInstruction?: boolean;
63
- readonly format?: "email" | "url" | "phone" | "password";
63
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
64
64
  readonly default?: string;
65
65
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
66
66
  readonly multiline?: boolean | {
@@ -95,7 +95,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
95
95
  readonly sensitive?: boolean;
96
96
  readonly writeOnly?: boolean;
97
97
  readonly readAsInstruction?: boolean;
98
- readonly format?: "email" | "url" | "phone" | "password";
98
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
99
99
  readonly default?: string;
100
100
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
101
101
  readonly multiline?: boolean | {
@@ -118,7 +118,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
118
118
  readonly sensitive?: boolean;
119
119
  readonly writeOnly?: boolean;
120
120
  readonly readAsInstruction?: boolean;
121
- readonly format?: "email" | "url" | "phone" | "password";
121
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
122
122
  readonly default?: string;
123
123
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
124
124
  readonly multiline?: boolean | {
@@ -141,7 +141,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
141
141
  readonly sensitive?: boolean;
142
142
  readonly writeOnly?: boolean;
143
143
  readonly readAsInstruction?: boolean;
144
- readonly format?: "email" | "url" | "phone" | "password";
144
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
145
145
  readonly default?: string;
146
146
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
147
147
  readonly multiline?: boolean | {
@@ -13,7 +13,7 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
13
13
  readonly sensitive?: boolean;
14
14
  readonly writeOnly?: boolean;
15
15
  readonly readAsInstruction?: boolean;
16
- readonly format?: "email" | "url" | "phone" | "password";
16
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
17
17
  readonly default?: string;
18
18
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
19
19
  readonly multiline?: boolean | {
@@ -36,7 +36,7 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
36
36
  readonly sensitive?: boolean;
37
37
  readonly writeOnly?: boolean;
38
38
  readonly readAsInstruction?: boolean;
39
- readonly format?: "email" | "url" | "phone" | "password";
39
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
40
40
  readonly default?: string;
41
41
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
42
42
  readonly multiline?: boolean | {
@@ -59,7 +59,7 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
59
59
  readonly sensitive?: boolean;
60
60
  readonly writeOnly?: boolean;
61
61
  readonly readAsInstruction?: boolean;
62
- readonly format?: "email" | "url" | "phone" | "password";
62
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
63
63
  readonly default?: string;
64
64
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
65
65
  readonly multiline?: boolean | {
@@ -94,7 +94,7 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
94
94
  readonly sensitive?: boolean;
95
95
  readonly writeOnly?: boolean;
96
96
  readonly readAsInstruction?: boolean;
97
- readonly format?: "email" | "url" | "phone" | "password";
97
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
98
98
  readonly default?: string;
99
99
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
100
100
  readonly multiline?: boolean | {
@@ -117,7 +117,7 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
117
117
  readonly sensitive?: boolean;
118
118
  readonly writeOnly?: boolean;
119
119
  readonly readAsInstruction?: boolean;
120
- readonly format?: "email" | "url" | "phone" | "password";
120
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
121
121
  readonly default?: string;
122
122
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
123
123
  readonly multiline?: boolean | {
@@ -140,7 +140,7 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
140
140
  readonly sensitive?: boolean;
141
141
  readonly writeOnly?: boolean;
142
142
  readonly readAsInstruction?: boolean;
143
- readonly format?: "email" | "url" | "phone" | "password";
143
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
144
144
  readonly default?: string;
145
145
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
146
146
  readonly multiline?: boolean | {
@@ -5,7 +5,7 @@ export { createFileRefFromBytes, createFileRefFromUploadedBytes, uploadFileBytes
5
5
  export { createFilesFeature, FILES_FEATURE } from "./feature.ts";
6
6
  export type { FileContext, FileHandle } from "./file-handle.ts";
7
7
  export { createFileContext, createTenantFileContext, deriveKey, storageKeyStemPrefix, } from "./file-handle.ts";
8
- export { fileRefEntity } from "./file-ref-entity.ts";
8
+ export { FILE_REF_ENTITY_NAME, fileRefEntity } from "./file-ref-entity.ts";
9
9
  export { fileRefsTable } from "./file-ref-table.ts";
10
10
  export type { FileAccessDecision, FileAccessGuard, FileRef, FileRoutesOptions, } from "./file-routes.ts";
11
11
  export { createFileRoutes, readFilesRouteOptions, resolveMaxUploadBodyBytes, } from "./file-routes.ts";
@@ -4,7 +4,7 @@ export { createFilesFeature, FILES_FEATURE } from "./feature.js";
4
4
  // `createFileHandle` is an implementation detail — construct handles via
5
5
  // `createFileContext(provider).ref(key)`, which is the AppContext surface.
6
6
  export { createFileContext, createTenantFileContext, deriveKey, storageKeyStemPrefix, } from "./file-handle.js";
7
- export { fileRefEntity } from "./file-ref-entity.js";
7
+ export { FILE_REF_ENTITY_NAME, fileRefEntity } from "./file-ref-entity.js";
8
8
  export { fileRefsTable } from "./file-ref-table.js";
9
9
  export { createFileRoutes, readFilesRouteOptions, resolveMaxUploadBodyBytes, } from "./file-routes.js";
10
10
  export { createInMemoryFileProvider } from "./in-memory-provider.js";
@@ -6,30 +6,30 @@ import type { ConfigAccessor, ConfigAccessorFactory, Registry } from "../engine/
6
6
  import type { SecretsContext } from "../secrets/index.ts";
7
7
  import type { FileStorageProvider } from "./types.ts";
8
8
  /**
9
- * Schmaler Surface-Type fuer Provider-Plugins. HandlerContext ist zu fett
10
- * (haelt tx, actor, signal etc.) — Provider sollen sich auf die read-Felder
11
- * beschraenken die fuer Tenant-Config + Secret-Lookup gebraucht werden.
9
+ * Narrow surface type for provider plugins. HandlerContext carries tx, actor,
10
+ * signal and more; providers only get the read fields needed for tenant config
11
+ * and secret lookup.
12
12
  *
13
- * **Warum nicht voller HandlerContext?** Im Worker-Pfad (r.job) gibt es keinen
14
- * request-bezogenen `tx`/`actor`/`signal`. Wenn ein Provider `ctx.tx` lesen
15
- * wuerde, wuerde der ganze Worker-Pfad zur Runtime brechen — und das wuerde NUR
16
- * mit S3 und nur in production auffallen. Die schmale Surface zwingt Provider
17
- * zur expliziten Erweiterung statt silent ctx-feld-ausnutzen.
13
+ * **Why not the full HandlerContext?** On the worker path (r.job) there is no
14
+ * request-bound `tx`/`actor`/`signal`. A provider reading `ctx.tx` would break
15
+ * the whole worker path at runtime, and that would only show up with S3 in
16
+ * production. The narrow surface forces providers to extend it explicitly
17
+ * instead of quietly relying on extra ctx fields.
18
18
  *
19
- * **Felder:**
20
- * config — fuer tenant-config-reads (bucket/region/endpoint/...)
21
- * registry — fuer extension-Lookup in der Factory (nicht Plugin-intern)
22
- * secrets — fuer tenant-secret-reads (s3.secretAccessKey)
23
- * _userId — Audit-/Authority-Identity fuer secret-reads. Der Framework-
24
- * Resolver setzt das auf SYSTEM_USER_ID — der s3-Provider liest
25
- * `s3.secretAccessKey`, was Nicht-Admin-Request-User nicht duerfen;
26
- * die Request-User-Autorisierung bleibt am Route-accessGuard.
19
+ * **Fields:**
20
+ * config — tenant config reads (bucket/region/endpoint/...)
21
+ * registry — extension lookup in the factory (not plugin-internal)
22
+ * secrets — tenant secret reads (s3.secretAccessKey)
23
+ * auditUserId — audit and authority identity for secret reads. The framework
24
+ * resolver sets SYSTEM_USER_ID because the s3 provider reads
25
+ * `s3.secretAccessKey`, which non-admin request users may not;
26
+ * request-user authorization stays on the route accessGuard.
27
27
  */
28
28
  export type FileProviderContext = {
29
29
  readonly config?: ConfigAccessor;
30
30
  readonly registry?: Registry;
31
31
  readonly secrets?: SecretsContext;
32
- readonly _userId?: string | undefined;
32
+ readonly auditUserId?: string | undefined;
33
33
  };
34
34
  /**
35
35
  * File-Storage-Plugin contract. Each provider-feature (file-provider-s3,
@@ -110,7 +110,7 @@ export function makeFileProviderResolver(deps) {
110
110
  db: deps.db,
111
111
  secrets: deps.secrets,
112
112
  });
113
- return createFileProviderForTenant({ config, registry: deps.registry, secrets: deps.secrets, _userId: SYSTEM_USER_ID }, tenantId);
113
+ return createFileProviderForTenant({ config, registry: deps.registry, secrets: deps.secrets, auditUserId: SYSTEM_USER_ID }, tenantId);
114
114
  })();
115
115
  built.catch(() => {
116
116
  if (cache.get(tenantId) === built)
@@ -0,0 +1,14 @@
1
+ import { type EgressPolicy, type ResolvedHost } from "./policy.ts";
2
+ type PinnedRequestInit = RequestInit & {
3
+ tls?: {
4
+ servername: string;
5
+ };
6
+ };
7
+ export declare function egress(policy: EgressPolicy): (raw: string, init?: RequestInit) => Promise<Response>;
8
+ export declare function withOriginalUrl(res: Response, originalUrl: URL): Response;
9
+ export declare function buildPinnedRequest(url: URL, resolved: ResolvedHost, init: RequestInit | undefined): {
10
+ url: URL;
11
+ init: PinnedRequestInit;
12
+ };
13
+ export declare function withManualRedirect(init: RequestInit | undefined): RequestInit;
14
+ export {};
@@ -0,0 +1,144 @@
1
+ import { assertAllowedTarget, assertHttpScheme, assertValidInternalPolicy, resolvePublicHost, } from "./policy.js";
2
+ const MAX_INTERNAL_REDIRECTS = 5;
3
+ // Single exported way to speak outward. The policy is named once, at bind
4
+ // time, and cannot drift from the request that follows it.
5
+ export function egress(policy) {
6
+ if (policy.kind === "internal")
7
+ assertValidInternalPolicy(policy);
8
+ return (raw, init) => runEgress(policy, raw, init);
9
+ }
10
+ async function runEgress(policy, raw, init) {
11
+ const url = new URL(raw);
12
+ assertHttpScheme(url);
13
+ if (policy.kind === "internal") {
14
+ assertAllowedTarget(url, policy);
15
+ return fetchWithAllowlistedRedirects(url, policy, init);
16
+ }
17
+ // external + tenant-supplied: deny private/reserved/link-local ranges,
18
+ // pin the connect to the exact address that passed that check (closes the
19
+ // DNS-rebinding TOCTOU, see buildPinnedRequest), and never follow a
20
+ // redirect — a 3xx could point anywhere, including back into the denied
21
+ // ranges above. The caller sees the 3xx response itself and, to follow it,
22
+ // is expected to call `egress()` again with the Location header — which
23
+ // re-runs this same resolve-and-pin check against the new host.
24
+ const resolved = await resolvePublicHost(url);
25
+ const pinned = buildPinnedRequest(url, resolved, init);
26
+ const res = await fetch(pinned.url, withManualRedirect(pinned.init));
27
+ return withOriginalUrl(res, url);
28
+ }
29
+ // fetch() reports `res.url` as the address it actually connected to — the
30
+ // pinned IP literal, not the hostname the caller asked for. Left alone, a
31
+ // caller resolving a relative Location header the normal way
32
+ // (`new URL(location, res.url)`) would resolve it against the IP instead of
33
+ // the original host, and the re-invoked egress() call would then pin and
34
+ // SNI-validate against that IP, breaking virtual hosting and TLS for
35
+ // essentially every real server. Overwriting `.url` back to the original
36
+ // request URL keeps that caller pattern working exactly as it would without
37
+ // IP pinning.
38
+ export function withOriginalUrl(res, originalUrl) {
39
+ Object.defineProperty(res, "url", { value: originalUrl.toString(), configurable: true });
40
+ return res;
41
+ }
42
+ // Rewrites the request to connect directly to `resolved.address` — the
43
+ // exact address `resolvePublicHost` just validated — instead of handing
44
+ // `fetch()` the original hostname and letting it resolve again. That second
45
+ // resolution is the DNS-rebinding window: fetch-by-IP removes it entirely,
46
+ // since there is no hostname left for fetch to look up. The original
47
+ // hostname is preserved in the Host header (virtual hosting) and in
48
+ // `tls.servername` (TLS SNI, and certificate validation still checks the
49
+ // real hostname — not the IP we dial).
50
+ export function buildPinnedRequest(url, resolved, init) {
51
+ const pinnedHost = resolved.family === 6 ? `[${resolved.address}]` : resolved.address;
52
+ const pinnedUrl = new URL(url.toString());
53
+ pinnedUrl.hostname = pinnedHost;
54
+ if (pinnedUrl.hostname !== pinnedHost) {
55
+ // The WHATWG URL hostname setter silently no-ops on invalid input
56
+ // instead of throwing. If that ever happened here, the request would
57
+ // silently fall back to the original (unpinned) hostname — reopening
58
+ // the rebinding window this function exists to close. Fail loudly.
59
+ throw new Error(`egress: failed to pin connection to resolved address ${resolved.address}`);
60
+ }
61
+ const headers = new Headers(init?.headers);
62
+ headers.set("host", url.host);
63
+ const pinnedInit = { ...init, headers };
64
+ if (url.protocol === "https:") {
65
+ pinnedInit.tls = { servername: url.hostname };
66
+ }
67
+ return { url: pinnedUrl, init: pinnedInit };
68
+ }
69
+ // Applied last, after spreading the caller's `init` — a caller cannot
70
+ // override the policy's redirect handling by passing its own `redirect`
71
+ // option. Used by both the external/tenant-supplied fetch above and the
72
+ // internal redirect loop below, so it is not part of the package's public
73
+ // barrel (not re-exported from index.ts) but is a plain named export for
74
+ // that shared use and for pinning the invariant directly in
75
+ // ./__tests__/egress.test.ts.
76
+ export function withManualRedirect(init) {
77
+ return { ...init, redirect: "manual" };
78
+ }
79
+ const ORIGIN_CHANGE_SAFE_HEADERS = new Set([
80
+ "content-type",
81
+ "accept",
82
+ "accept-encoding",
83
+ "accept-language",
84
+ "user-agent",
85
+ ]);
86
+ // Allowlist, not denylist: any credential-bearing header we have not thought
87
+ // of (x-api-key, custom tokens) must not follow a redirect to another origin.
88
+ function stripToSafeHeaders(init) {
89
+ const kept = new Headers();
90
+ for (const [name, value] of new Headers(init?.headers)) {
91
+ if (ORIGIN_CHANGE_SAFE_HEADERS.has(name))
92
+ kept.set(name, value);
93
+ }
94
+ return { ...init, headers: kept };
95
+ }
96
+ // `internal` is the only kind that permits redirects (it is the only kind
97
+ // with an allowlist to re-validate them against). Each hop's Location is
98
+ // resolved, restricted to the current hop's host and checked against the
99
+ // policy before it is followed, capped at MAX_INTERNAL_REDIRECTS.
100
+ // `init` (method, body) is replayed verbatim on every hop — unlike a browser,
101
+ // this does not downgrade POST to GET on a 301/302/303. Headers are replayed
102
+ // only while the origin is unchanged; once a hop leaves the original origin
103
+ // (scheme or port differs), credential headers are dropped for the rest of the
104
+ // chain. The body is still replayed to the new origin (as fetch does on
105
+ // 307/308); use allowOrigins to forbid such hops entirely.
106
+ async function fetchWithAllowlistedRedirects(url, policy, init) {
107
+ let current = url;
108
+ let hopInit = init;
109
+ let strippedInit;
110
+ for (let hop = 0; hop <= MAX_INTERNAL_REDIRECTS; hop++) {
111
+ const res = await fetch(current, withManualRedirect(hopInit));
112
+ if (res.status < 300 || res.status >= 400)
113
+ return res;
114
+ const location = res.headers.get("location");
115
+ if (!location)
116
+ return res; // 3xx without Location — nothing to follow
117
+ let next;
118
+ try {
119
+ next = new URL(location, current);
120
+ }
121
+ catch {
122
+ throw new Error(`egress(internal): redirect Location header is not a resolvable URL: ${location}`);
123
+ }
124
+ assertHttpScheme(next);
125
+ // Hosts may differ in the allowlist; credentials must still not hop
126
+ // between them.
127
+ if (next.hostname.toLowerCase() !== current.hostname.toLowerCase()) {
128
+ throw new Error(`egress(internal): redirect crosses host (${current.hostname} -> ${next.hostname}), not followed`);
129
+ }
130
+ try {
131
+ assertAllowedTarget(next, policy);
132
+ }
133
+ catch {
134
+ // Prefix is the contract consumers match on (chat-webhook-sender); origins only, no path/query.
135
+ throw new Error(`egress(internal): redirect target not in allowlist (${current.origin} -> ${next.origin}), not followed`);
136
+ }
137
+ if (next.origin !== url.origin) {
138
+ strippedInit ??= stripToSafeHeaders(init);
139
+ hopInit = strippedInit;
140
+ }
141
+ current = next;
142
+ }
143
+ throw new Error(`egress(internal): exceeded ${MAX_INTERNAL_REDIRECTS} redirects`);
144
+ }
@@ -1,2 +1,3 @@
1
- export type { EgressPolicy, ResolvedHost } from "@cosmicdrift/kumiko-http";
2
- export { BlockedHostError, buildPinnedRequest, egress, HostResolutionError, isPublicHost, resolvePublicHost, resolvePublicHostname, } from "@cosmicdrift/kumiko-http";
1
+ export { buildPinnedRequest, egress } from "./egress.ts";
2
+ export type { EgressPolicy, ResolvedHost } from "./policy.ts";
3
+ export { BlockedHostError, HostResolutionError, isPublicHost, resolvePublicHost, resolvePublicHostname, } from "./policy.ts";
@@ -1 +1,2 @@
1
- export { BlockedHostError, buildPinnedRequest, egress, HostResolutionError, isPublicHost, resolvePublicHost, resolvePublicHostname, } from "@cosmicdrift/kumiko-http";
1
+ export { buildPinnedRequest, egress } from "./egress.js";
2
+ export { BlockedHostError, HostResolutionError, isPublicHost, resolvePublicHost, resolvePublicHostname, } from "./policy.js";