@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.
- package/AGENTS.md +2 -2
- package/agents/examples/apex-landing.md +1 -1
- package/agents/examples/apex-surface-auth.md +3 -3
- package/agents/examples/compose-stacks.md +15 -8
- package/agents/examples/embedded-entity-form.md +1 -1
- package/agents/examples/notes-history-basic.md +1 -1
- package/agents/examples/tags-basic.md +1 -1
- package/agents/examples/user-profile.md +1 -1
- package/agents/examples/wizard-form.md +4 -4
- package/agents/widgets.md +3 -3
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/routes.js +2 -2
- package/dist/api/server.js +2 -1
- package/dist/config/index.d.ts +4 -0
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +1 -1
- package/dist/db/migrate-runner.js +12 -4
- package/dist/db/money.js +1 -1
- package/dist/db/query.js +14 -14
- package/dist/db/replay-migration-sql.d.ts +1 -0
- package/dist/db/replay-migration-sql.js +115 -11
- package/dist/db/tenant-db.d.ts +7 -0
- package/dist/db/tenant-db.js +17 -5
- package/dist/engine/boot-validator/screens.js +7 -1
- package/dist/engine/index.d.ts +2 -1
- package/dist/engine/index.js +1 -0
- package/dist/engine/parse-ref-target.js +1 -1
- package/dist/engine/provider-plugin-context.d.ts +3 -0
- package/dist/engine/provider-plugin-context.js +28 -0
- package/dist/engine/schema-builder.js +7 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/env/index.js +1 -1
- package/dist/errors/zod-bridge.js +1 -1
- package/dist/files/file-ref-entity.d.ts +6 -6
- package/dist/files/file-ref-table.d.ts +6 -6
- package/dist/files/index.d.ts +1 -1
- package/dist/files/index.js +1 -1
- package/dist/files/provider-resolver.d.ts +17 -17
- package/dist/files/provider-resolver.js +1 -1
- package/dist/http/egress.d.ts +14 -0
- package/dist/http/egress.js +144 -0
- package/dist/http/index.d.ts +3 -2
- package/dist/http/index.js +2 -1
- package/dist/http/policy.d.ts +30 -0
- package/dist/http/policy.js +299 -0
- package/dist/i18n/required-surface-keys.js +2 -0
- package/dist/jobs/job-runner.js +4 -2
- package/dist/pipeline/config-accessor-factory.d.ts +3 -0
- package/dist/pipeline/config-accessor-factory.js +3 -0
- package/dist/pipeline/dispatch-shared.js +7 -5
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/upgrade-changelog.d.ts +25 -0
- package/dist/upgrade-changelog.js +48 -0
- package/dist/upgrade-cli.js +78 -13
- package/package.json +3 -5
- package/presets/biome-app.json +8 -0
- package/src/changes.json +71 -0
- package/src/scripts/codemod/README.md +25 -0
- package/src/scripts/codemod/codemod-support.ts +64 -6
- package/src/scripts/codemod/package-merge.ts +260 -0
- package/src/scripts/codemod/s3-imports.ts +4 -42
- package/src/scripts/codemod/testing-injection.ts +135 -0
package/dist/db/tenant-db.d.ts
CHANGED
|
@@ -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;
|
package/dist/db/tenant-db.js
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
|
309
|
-
"
|
|
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
|
|
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 — ` +
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/engine/index.js
CHANGED
|
@@ -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:
|
|
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/
|
|
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 | {
|
package/dist/files/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/files/index.js
CHANGED
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
* **
|
|
14
|
-
* request-
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
* **
|
|
20
|
-
* config
|
|
21
|
-
* registry
|
|
22
|
-
* secrets
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
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,
|
|
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
|
+
}
|
package/dist/http/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
export {
|
|
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";
|
package/dist/http/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { buildPinnedRequest, egress } from "./egress.js";
|
|
2
|
+
export { BlockedHostError, HostResolutionError, isPublicHost, resolvePublicHost, resolvePublicHostname, } from "./policy.js";
|