@cosmicdrift/kumiko-bundled-features 0.354.1 → 0.356.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-tools/agent-manifest.js +2 -0
- package/dist/agent-tools/types.d.ts +2 -0
- package/dist/audit/escape-hatch-audit-sink.d.ts +1 -0
- package/dist/audit/escape-hatch-audit-sink.js +2 -0
- package/dist/audit/handlers/resolve-audit-tenant.js +8 -4
- package/dist/billing-foundation/consumer-protection/register-consent.js +1 -2
- package/dist/billing-foundation/consumer-protection/register-termination.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/register-termination.js +1 -2
- package/dist/cap-overview/handlers/caps-usage.query.js +8 -5
- package/dist/compliance-profiles/handlers/set-profile.write.js +15 -26
- package/dist/custom-fields/handlers/define-system-field.write.js +8 -16
- package/dist/custom-fields/handlers/delete-system-field.write.js +5 -2
- package/dist/custom-fields/lib/define-or-resurrect.d.ts +2 -1
- package/dist/custom-fields/lib/define-or-resurrect.js +11 -4
- package/dist/delivery/delivery-service.js +1 -1
- package/dist/managed-pages/handlers/by-tenant-published.query.js +1 -4
- package/dist/managed-pages/handlers/set.write.js +7 -22
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/require-for-tenant.d.ts +2 -0
- package/dist/shared/require-for-tenant.js +9 -0
- package/dist/template-resolver/handlers/by-slug.query.js +1 -4
- package/dist/template-resolver/handlers/by-tenant.query.js +1 -4
- package/dist/template-resolver/handlers/collection-item.query.js +1 -4
- package/dist/template-resolver/handlers/collection-list.query.js +1 -4
- package/dist/template-resolver/handlers/collection-set.write.js +5 -9
- package/dist/template-resolver/handlers/set.write.js +7 -12
- package/dist/template-resolver/handlers/upsert-system.write.js +6 -6
- package/dist/template-resolver/handlers/upsert-tenant.write.js +7 -8
- package/dist/template-resolver/seeding.js +1 -2
- package/dist/tenant/cancel-pending-invitation.d.ts +2 -2
- package/dist/tenant/cancel-pending-invitation.js +2 -2
- package/dist/tenant/handlers/remove-member.write.js +13 -10
- package/dist/tenant/handlers/update-member-roles.write.js +12 -9
- package/dist/tenant-lifecycle/feature.js +1 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +3 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -0
- package/dist/tenant-lifecycle/stages.d.ts +2 -0
- package/dist/tenant-lifecycle/stages.js +2 -0
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +7 -18
- package/dist/user-data-rights/feature.js +2 -0
- package/dist/user-data-rights/run-export-jobs.d.ts +2 -0
- package/dist/user-data-rights/run-export-jobs.js +2 -0
- package/dist/user-data-rights/run-forget-cleanup.d.ts +2 -0
- package/dist/user-data-rights/run-forget-cleanup.js +4 -0
- package/dist/user-data-rights/run-user-export.d.ts +2 -0
- package/dist/user-data-rights/run-user-export.js +2 -0
- package/package.json +9 -9
|
@@ -163,6 +163,7 @@ function buildHandlerEntry(qn, kind, def, roles, getHandlerEntity, denyQns) {
|
|
|
163
163
|
return undefined;
|
|
164
164
|
}
|
|
165
165
|
const entity = getHandlerEntity(qn);
|
|
166
|
+
const excludedFields = "excludedFields" in def ? def.excludedFields : undefined;
|
|
166
167
|
return {
|
|
167
168
|
qn,
|
|
168
169
|
kind,
|
|
@@ -170,6 +171,7 @@ function buildHandlerEntry(qn, kind, def, roles, getHandlerEntity, denyQns) {
|
|
|
170
171
|
risk: exposure.risk,
|
|
171
172
|
inputSchema,
|
|
172
173
|
...(entity !== undefined && { entity }),
|
|
174
|
+
...(excludedFields !== undefined && { excludedFields }),
|
|
173
175
|
};
|
|
174
176
|
}
|
|
175
177
|
function buildHandlers(queryHandlers, writeHandlers, roles, getHandlerEntity, denyQns) {
|
|
@@ -130,6 +130,8 @@ export type AgentManifestHandler = {
|
|
|
130
130
|
readonly risk: AgentRisk;
|
|
131
131
|
readonly inputSchema: Readonly<Record<string, unknown>>;
|
|
132
132
|
readonly entity?: string;
|
|
133
|
+
/** Fields this generic create/update handler never writes; sending one fails validation. */
|
|
134
|
+
readonly excludedFields?: readonly string[];
|
|
133
135
|
};
|
|
134
136
|
export type AgentManifestScreen = {
|
|
135
137
|
readonly id: string;
|
|
@@ -15,6 +15,7 @@ export declare const escapeHatchUsedSchema: z.ZodObject<{
|
|
|
15
15
|
}>;
|
|
16
16
|
reason: z.ZodString;
|
|
17
17
|
actor: z.ZodString;
|
|
18
|
+
caller: z.ZodOptional<z.ZodString>;
|
|
18
19
|
targetUserId: z.ZodOptional<z.ZodString>;
|
|
19
20
|
targetTenantId: z.ZodOptional<z.ZodString>;
|
|
20
21
|
}, z.core.$strip>;
|
|
@@ -18,6 +18,7 @@ export const escapeHatchUsedSchema = z.object({
|
|
|
18
18
|
kind: z.enum(escapeHatchKinds),
|
|
19
19
|
reason: z.string(),
|
|
20
20
|
actor: z.string(),
|
|
21
|
+
caller: z.string().optional(),
|
|
21
22
|
targetUserId: z.string().optional(),
|
|
22
23
|
targetTenantId: z.string().optional(),
|
|
23
24
|
});
|
|
@@ -28,6 +29,7 @@ export function createEscapeHatchAuditSink(opts) {
|
|
|
28
29
|
kind: event.kind,
|
|
29
30
|
reason: event.reason,
|
|
30
31
|
actor: event.actor,
|
|
32
|
+
...(event.caller !== undefined && { caller: event.caller }),
|
|
31
33
|
...(event.target && { targetUserId: event.target.id, targetTenantId: event.target.tenantId }),
|
|
32
34
|
});
|
|
33
35
|
// Appends on the unbound pool, never the handler tx — an audit entry must
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mayOverrideTenant } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { AccessDeniedError, FrameworkReasons } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
3
|
import { APP_INSTANCE_STREAM_TYPE } from "@cosmicdrift/kumiko-framework/event-store";
|
|
3
4
|
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-types/identifiers";
|
|
4
5
|
import { AuditScopes } from "../constants.js";
|
|
@@ -7,8 +8,11 @@ import { AuditScopes } from "../constants.js";
|
|
|
7
8
|
export function resolveAuditScopeFilter(user, scope) {
|
|
8
9
|
if (scope !== AuditScopes.system)
|
|
9
10
|
return { tenantId: user.tenantId };
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
if (!mayOverrideTenant(user)) {
|
|
12
|
+
throw new AccessDeniedError({
|
|
13
|
+
i18nKey: "audit.errors.systemScopeRequiresSystemAdmin",
|
|
14
|
+
details: { reason: FrameworkReasons.tenantOverrideRequiresSystemAdmin },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
13
17
|
return { tenantId: SYSTEM_TENANT_ID, aggregateType: APP_INSTANCE_STREAM_TYPE };
|
|
14
18
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
1
|
import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id.js";
|
|
3
2
|
import { SubscriptionFoundationHandlers, SubscriptionStatuses } from "../constants.js";
|
|
4
3
|
import { CHECKOUT_CONSENT_RECORDED_EVENT_SHORT, CONTRACT_CONFIRMATION_ISSUED_EVENT_SHORT, checkoutConsentRecordedPayloadSchema, contractConfirmationIssuedPayloadSchema, INVOICE_PAID_EVENT_QN, PAYMENT_RECEIVED_EVENT_QN, SUBSCRIPTION_CREATED_EVENT_QN, SUBSCRIPTION_UPDATED_EVENT_QN, } from "../events.js";
|
|
@@ -48,7 +47,7 @@ export function registerConsumerProtection(r, options, consumerProtection) {
|
|
|
48
47
|
// user), never from payload data.
|
|
49
48
|
const tenantId = ctx.systemUser.tenantId;
|
|
50
49
|
const currentPeriodEndIso = payload["currentPeriodEndIso"];
|
|
51
|
-
const result = await ctx.
|
|
50
|
+
const result = await ctx.write(SubscriptionFoundationHandlers.issueContractConfirmation, {
|
|
52
51
|
consentId,
|
|
53
52
|
sourceAggregateId: isSubscriptionEvent
|
|
54
53
|
? subscriptionAggregateId(tenantId)
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from "../types.js";
|
|
3
3
|
export declare function registerContractTermination(r: FeatureRegistrar, options: ResolvedBillingFoundationOptions, consumerProtection: ConsumerProtectionOptions): void;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
1
|
import { SubscriptionCancelTimings, SubscriptionFoundationHandlers } from "../constants.js";
|
|
3
2
|
import { CONTRACT_TERMINATION_DECLARED_EVENT_QN, CONTRACT_TERMINATION_DECLARED_EVENT_SHORT, CONTRACT_TERMINATION_REQUESTED_EVENT_SHORT, CONTRACT_TERMINATION_UNMATCHED_EVENT_SHORT, contractTerminationDeclaredPayloadSchema, contractTerminationRequestedPayloadSchema, contractTerminationUnmatchedPayloadSchema, } from "../events.js";
|
|
4
3
|
import { createDeclareContractTerminationHandler, createRecordContractTerminationHandler, createRecordUnmatchedContractTerminationHandler, createTerminableSubscriptionQuery, } from "./termination-record.js";
|
|
@@ -22,7 +21,7 @@ export function registerContractTermination(r, options, consumerProtection) {
|
|
|
22
21
|
// skip: trigger event carries no usable declaration
|
|
23
22
|
if (!declared.success)
|
|
24
23
|
return;
|
|
25
|
-
const result = await ctx.
|
|
24
|
+
const result = await ctx.write(SubscriptionFoundationHandlers.recordContractTermination, {
|
|
26
25
|
requestId: declared.data.requestId,
|
|
27
26
|
declarationType: declared.data.declarationType,
|
|
28
27
|
terminationKind: declared.data.terminationKind,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import {
|
|
4
|
-
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { defineQueryHandler, mayOverrideTenant, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import { AccessDeniedError, FrameworkReasons, InternalError, } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
5
|
import * as z from "zod";
|
|
6
6
|
import { tierAssignmentEntity } from "../../tier-engine/index.js";
|
|
7
7
|
import { computeFraction, computeTone, computeUnclampedFraction } from "../usage-math.js";
|
|
@@ -24,9 +24,12 @@ export function createCapsUsageQuery(caps, usageRoles) {
|
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
const override = query.payload.tenantId;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
if (override !== undefined && !mayOverrideTenant(query.user)) {
|
|
28
|
+
throw new AccessDeniedError({
|
|
29
|
+
i18nKey: "cap-overview.errors.tenantOverrideRequiresSystemAdmin",
|
|
30
|
+
details: { reason: FrameworkReasons.tenantOverrideRequiresSystemAdmin },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
30
33
|
const targetTenantId = override ?? query.user.tenantId;
|
|
31
34
|
// Both branches return the SAME unfiltered system-mode db —
|
|
32
35
|
// assertTenantMatch is a self-check on the caller, not a query
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { complianceProfileOverrideSchema, SELECTABLE_PROFILE_KEYS, } from "@cosmicdrift/kumiko-framework/compliance";
|
|
3
3
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
4
|
-
import { access,
|
|
4
|
+
import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
5
|
import { UnprocessableError, validationErrorFromZod, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
6
6
|
import * as z from "zod";
|
|
7
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
7
8
|
import { tenantComplianceProfileEntity, tenantComplianceProfileTable, } from "../schema/profile-selection.js";
|
|
8
9
|
const crud = createEventStoreExecutor(tenantComplianceProfileTable, tenantComplianceProfileEntity, {
|
|
9
10
|
entityName: "tenant-compliance-profile",
|
|
@@ -13,24 +14,16 @@ const crud = createEventStoreExecutor(tenantComplianceProfileTable, tenantCompli
|
|
|
13
14
|
// nicht eine waehlbare Production-Option. Symmetrisch zu
|
|
14
15
|
// SELECTABLE_PROFILE_KEYS aus der framework/compliance-Liste.
|
|
15
16
|
const profileKeySchema = z.enum(SELECTABLE_PROFILE_KEYS);
|
|
16
|
-
//
|
|
17
|
+
// Upsert: the first call inserts, later calls update, so repeating the same values
|
|
18
|
+
// is idempotent apart from the extra audit events.
|
|
17
19
|
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
20
|
+
// A SystemAdmin may target another tenant via `tenantIdOverride` (operator setup,
|
|
21
|
+
// onboarding migrations); the dispatcher rejects the override for anyone else. The
|
|
22
|
+
// executor then writes into the target tenant's stream via streamTenantId while
|
|
23
|
+
// the operator stays the recorded actor.
|
|
21
24
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// Onboarding-Migrationen). TenantAdmin's Override-Versuch → 403.
|
|
25
|
-
// executorUser.tenantId muss = ziel-tenant sein damit der event-store-
|
|
26
|
-
// Stream-Lookup nicht miss → version_conflict gibt (Memory:
|
|
27
|
-
// feedback_event_store_tenant_consistency).
|
|
28
|
-
//
|
|
29
|
-
// Validation:
|
|
30
|
-
// - profileKey muss in SELECTABLE_PROFILE_KEYS sein (Zod-checked)
|
|
31
|
-
// - override (optional) muss valides JSON-Object sein
|
|
32
|
-
// - override Top-Level-Keys muessen in ALLOWED_OVERRIDE_KEYS sein
|
|
33
|
-
// — verhindert Tippfehler die deepMerge stillschweigend ignoriert
|
|
25
|
+
// Override top-level keys are restricted to ALLOWED_OVERRIDE_KEYS because deepMerge
|
|
26
|
+
// would silently ignore a typo.
|
|
34
27
|
export const setProfileWrite = defineWriteHandler({
|
|
35
28
|
name: "set-profile",
|
|
36
29
|
schema: z.object({
|
|
@@ -43,12 +36,8 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
43
36
|
access: { roles: access.admin },
|
|
44
37
|
description: "Sets or replaces a tenant's compliance profile key plus an optional JSON override, for onboarding or a later region change; a system admin may target a different tenant through tenantIdOverride.",
|
|
45
38
|
handler: async (event, ctx) => {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
if (overrideDenied)
|
|
49
|
-
return writeFailure(overrideDenied);
|
|
50
|
-
const tenantId = (tenantOverride ?? event.user.tenantId); // @cast-boundary engine-payload
|
|
51
|
-
const executorUser = tenantOverride !== undefined ? { ...event.user, tenantId } : event.user;
|
|
39
|
+
const tenantId = (event.payload.tenantIdOverride ?? event.user.tenantId); // @cast-boundary engine-payload
|
|
40
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
52
41
|
// Override-Validation: muss parseables JSON-Object sein UND dem
|
|
53
42
|
// ComplianceProfileOverride-Schema entsprechen (S1.9 Z3 — strict-Zod
|
|
54
43
|
// mit Top-Level + Sub-Level-Whitelist via .strict()). Tippfehler
|
|
@@ -75,7 +64,7 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
75
64
|
}
|
|
76
65
|
}
|
|
77
66
|
// Upsert: existierenden Eintrag suchen
|
|
78
|
-
const existing = (await fetchOne(
|
|
67
|
+
const existing = (await fetchOne(db, tenantComplianceProfileTable, {
|
|
79
68
|
tenantId: tenantId,
|
|
80
69
|
})); // @cast-boundary db-runner
|
|
81
70
|
if (existing) {
|
|
@@ -86,7 +75,7 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
86
75
|
profileKey: event.payload.profileKey,
|
|
87
76
|
override: event.payload.override ?? null,
|
|
88
77
|
},
|
|
89
|
-
},
|
|
78
|
+
}, event.user, db, { streamTenantId });
|
|
90
79
|
if (!result.isSuccess)
|
|
91
80
|
return result;
|
|
92
81
|
return {
|
|
@@ -98,7 +87,7 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
98
87
|
profileKey: event.payload.profileKey,
|
|
99
88
|
override: event.payload.override ?? null,
|
|
100
89
|
tenantId,
|
|
101
|
-
},
|
|
90
|
+
}, event.user, db, { streamTenantId });
|
|
102
91
|
if (!result.isSuccess)
|
|
103
92
|
return result;
|
|
104
93
|
return {
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
2
3
|
import { fieldDefinitionAggregateId } from "../aggregate-id.js";
|
|
3
4
|
import { defineOrResurrectFieldDefinition } from "../lib/define-or-resurrect.js";
|
|
4
5
|
import { buildFieldDefinitionColumns } from "../lib/field-definition-row.js";
|
|
5
6
|
import { defineFieldPayloadSchema } from "../schemas.js";
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// **Use-Case:** Vendor (cdgs) sagt "alle Hausverwaltungs-customers haben ab
|
|
12
|
-
// heute ein `internalNumber`-Field". Tenant kann den Wert pro customer
|
|
13
|
-
// setzen, aber die Definition nicht ändern oder löschen.
|
|
14
|
-
//
|
|
15
|
-
// **Same-scope-conflict** wie bei define-tenant-field via aggregate-version-
|
|
16
|
-
// conflict (deterministische ID mit tenantId=SYSTEM_TENANT_ID).
|
|
7
|
+
// A system-wide field definition applies to every tenant. tenantId is forced to
|
|
8
|
+
// SYSTEM_TENANT_ID (never taken from the caller) and addressed via streamTenantId,
|
|
9
|
+
// so the SystemAdmin stays the recorded actor. Tenants can set values for the field
|
|
10
|
+
// but cannot change or delete the definition. A same-scope duplicate surfaces as an
|
|
11
|
+
// aggregate version conflict, because the ID is derived from SYSTEM_TENANT_ID.
|
|
17
12
|
export const defineSystemFieldHandler = {
|
|
18
13
|
name: "define-system-field",
|
|
19
14
|
schema: defineFieldPayloadSchema,
|
|
@@ -22,10 +17,7 @@ export const defineSystemFieldHandler = {
|
|
|
22
17
|
handler: async (event, ctx) => {
|
|
23
18
|
const payload = event.payload; // @cast-boundary engine-payload
|
|
24
19
|
const aggregateId = fieldDefinitionAggregateId(SYSTEM_TENANT_ID, payload.entityName, payload.fieldKey);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// — the row lives in the system-scope-stream.
|
|
28
|
-
const systemUser = { ...event.user, tenantId: SYSTEM_TENANT_ID };
|
|
29
|
-
return defineOrResurrectFieldDefinition(aggregateId, buildFieldDefinitionColumns(payload), systemUser, ctx.db);
|
|
20
|
+
const { db, streamTenantId } = requireForTenant(ctx, SYSTEM_TENANT_ID);
|
|
21
|
+
return defineOrResurrectFieldDefinition(aggregateId, buildFieldDefinitionColumns(payload), event.user, db, streamTenantId);
|
|
30
22
|
},
|
|
31
23
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
2
3
|
import { fieldDefinitionAggregateId } from "../aggregate-id.js";
|
|
3
4
|
import { FIELD_DEFINITION_AGGREGATE_TYPE } from "../constants.js";
|
|
4
5
|
import { fieldDefinitionExecutor } from "../executor.js";
|
|
@@ -18,8 +19,10 @@ export const deleteSystemFieldHandler = {
|
|
|
18
19
|
handler: async (event, ctx) => {
|
|
19
20
|
const payload = event.payload; // @cast-boundary engine-payload
|
|
20
21
|
const aggregateId = fieldDefinitionAggregateId(SYSTEM_TENANT_ID, payload.entityName, payload.fieldKey);
|
|
21
|
-
const
|
|
22
|
-
const result = await fieldDefinitionExecutor.delete({ id: aggregateId },
|
|
22
|
+
const { db, streamTenantId } = requireForTenant(ctx, SYSTEM_TENANT_ID);
|
|
23
|
+
const result = await fieldDefinitionExecutor.delete({ id: aggregateId }, event.user, db, {
|
|
24
|
+
streamTenantId,
|
|
25
|
+
});
|
|
23
26
|
// Cascade-cleanup-Event — host-entity-MSPs entfernen orphan values aus
|
|
24
27
|
// ihrer customFields jsonb. Im selben TX = atomic.
|
|
25
28
|
if (result.isSuccess) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
2
|
import { fieldDefinitionExecutor } from "../executor.js";
|
|
2
3
|
import type { FieldDefinitionColumns } from "./field-definition-row.js";
|
|
3
4
|
type DefineUser = Parameters<typeof fieldDefinitionExecutor.create>[1];
|
|
4
5
|
type DefineDb = Parameters<typeof fieldDefinitionExecutor.create>[2];
|
|
5
6
|
type WriteResult = Awaited<ReturnType<typeof fieldDefinitionExecutor.create>>;
|
|
6
|
-
export declare function defineOrResurrectFieldDefinition(aggregateId: string, columns: FieldDefinitionColumns, user: DefineUser, db: DefineDb): Promise<WriteResult>;
|
|
7
|
+
export declare function defineOrResurrectFieldDefinition(aggregateId: string, columns: FieldDefinitionColumns, user: DefineUser, db: DefineDb, streamTenantId?: TenantId): Promise<WriteResult>;
|
|
7
8
|
export {};
|
|
@@ -16,12 +16,16 @@ import { fieldDefinitionExecutor } from "../executor.js";
|
|
|
16
16
|
// fail with "current transaction is aborted". detail() (a read) + restore()
|
|
17
17
|
// (which sees soft-deleted rows via a tenant-scoped fetch and only writes on
|
|
18
18
|
// success) keep the tx clean until the single terminal write.
|
|
19
|
-
export async function defineOrResurrectFieldDefinition(aggregateId, columns, user, db) {
|
|
19
|
+
export async function defineOrResurrectFieldDefinition(aggregateId, columns, user, db, streamTenantId) {
|
|
20
20
|
const active = await fieldDefinitionExecutor.detail({ id: aggregateId }, user, db);
|
|
21
21
|
if (active) {
|
|
22
|
-
return fieldDefinitionExecutor.create({ id: aggregateId, ...columns }, user, db
|
|
22
|
+
return fieldDefinitionExecutor.create({ id: aggregateId, ...columns }, user, db, {
|
|
23
|
+
streamTenantId,
|
|
24
|
+
});
|
|
23
25
|
}
|
|
24
|
-
const restored = await fieldDefinitionExecutor.restore({ id: aggregateId }, user, db
|
|
26
|
+
const restored = await fieldDefinitionExecutor.restore({ id: aggregateId }, user, db, {
|
|
27
|
+
streamTenantId,
|
|
28
|
+
});
|
|
25
29
|
if (restored.isSuccess) {
|
|
26
30
|
// restore() just un-deleted the row in this same tx; no concurrent writer
|
|
27
31
|
// exists, so skip the optimistic-lock version match (we'd otherwise have to
|
|
@@ -31,10 +35,13 @@ export async function defineOrResurrectFieldDefinition(aggregateId, columns, use
|
|
|
31
35
|
// readonly FieldDefinitionColumns isn't assignable to it.
|
|
32
36
|
return fieldDefinitionExecutor.update({ id: aggregateId, changes: { ...columns } }, user, db, {
|
|
33
37
|
skipOptimisticLock: true,
|
|
38
|
+
streamTenantId,
|
|
34
39
|
});
|
|
35
40
|
}
|
|
36
41
|
if (restored.error.code === "not_found") {
|
|
37
|
-
return fieldDefinitionExecutor.create({ id: aggregateId, ...columns }, user, db
|
|
42
|
+
return fieldDefinitionExecutor.create({ id: aggregateId, ...columns }, user, db, {
|
|
43
|
+
streamTenantId,
|
|
44
|
+
});
|
|
38
45
|
}
|
|
39
46
|
return restored;
|
|
40
47
|
}
|
|
@@ -92,7 +92,7 @@ export function createDeliveryService(options) {
|
|
|
92
92
|
tenantId,
|
|
93
93
|
actor: systemUser.id,
|
|
94
94
|
sink: escapeHatchAuditSink,
|
|
95
|
-
log,
|
|
95
|
+
log: logError,
|
|
96
96
|
});
|
|
97
97
|
// ctx.db/dbOutsideTransaction are tenant-filtered even for a systemScope handler:
|
|
98
98
|
// the dispatcher closes them off entirely there, so "system" mode would hand this
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { castTenantRows } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import {
|
|
3
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import * as z from "zod";
|
|
5
5
|
import { pagesTable } from "../table.js";
|
|
6
6
|
export const byTenantPublishedQuery = defineQueryHandler({
|
|
@@ -15,9 +15,6 @@ export const byTenantPublishedQuery = defineQueryHandler({
|
|
|
15
15
|
description: "Lists every published page of a tenant with slug, language, title and last-change time but no body; use it to enumerate the public pages for sitemap.xml or llms.txt, rather than by-slug which fetches one page's content.",
|
|
16
16
|
handler: async (query, ctx) => {
|
|
17
17
|
const override = query.payload.tenantIdOverride;
|
|
18
|
-
const overrideDenied = crossTenantOverrideDenied(query.user, override, "managedPages.errors.tenantOverrideRequiresSystemAdmin");
|
|
19
|
-
if (overrideDenied)
|
|
20
|
-
throw overrideDenied;
|
|
21
18
|
const tenantId = override ?? query.user.tenantId;
|
|
22
19
|
const rows = castTenantRows(await selectMany(ctx.db, pagesTable, { tenantId: tenantId, published: true }));
|
|
23
20
|
return {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import { createEventStoreExecutor
|
|
3
|
-
import {
|
|
4
|
-
import { writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
|
+
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
4
|
import * as z from "zod";
|
|
5
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
6
6
|
import { pageEntity, pagesTable } from "../table.js";
|
|
7
7
|
const slugSchema = z
|
|
8
8
|
.string()
|
|
@@ -15,7 +15,6 @@ const langSchema = z
|
|
|
15
15
|
.max(8)
|
|
16
16
|
.regex(/^[a-z]{2}(-[a-z]{2})?$/i, "lang must be ISO 639-1 (e.g. de, en, en-us)");
|
|
17
17
|
const executor = createEventStoreExecutor(pagesTable, pageEntity, { entityName: "page" });
|
|
18
|
-
const SET_PAGE_TENANT_OVERRIDE_REASON = "SystemAdmin tenantIdOverride re-scopes the page write to the target tenant";
|
|
19
18
|
// Upsert einer Page — eine Operation pro (tenantId, slug, lang). Tenant-
|
|
20
19
|
// Scope default aus event.user; SystemAdmin kann via `tenantIdOverride`
|
|
21
20
|
// für einen anderen Tenant schreiben (typisch SYSTEM_TENANT_ID für app-
|
|
@@ -37,26 +36,12 @@ export const setWrite = defineWriteHandler({
|
|
|
37
36
|
}),
|
|
38
37
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
39
38
|
description: "Creates or overwrites one managed page addressed by slug and language, keeping the existing published flag, description and OG image when the payload omits them; use it for content edits and publish toggles, and as SystemAdmin to write another tenant's pages.",
|
|
40
|
-
escapeHatch: {
|
|
41
|
-
grants: ["unsafeRaw"],
|
|
42
|
-
reason: SET_PAGE_TENANT_OVERRIDE_REASON,
|
|
43
|
-
},
|
|
44
39
|
handler: async (event, ctx) => {
|
|
45
|
-
const db = ctx.db;
|
|
46
40
|
const override = event.payload.tenantIdOverride;
|
|
47
|
-
const
|
|
48
|
-
if (overrideDenied)
|
|
49
|
-
return writeFailure(overrideDenied);
|
|
50
|
-
const tenantId = override ?? event.user.tenantId;
|
|
51
|
-
// override: point the executor context at the target tenant, else getStreamVersion runs against user.tenantId → version_conflict.
|
|
52
|
-
const executorUser = override !== undefined ? { ...event.user, tenantId: override } : event.user; // @cast-boundary engine-bridge
|
|
41
|
+
const tenantId = (override ?? event.user.tenantId); // @cast-boundary engine-bridge
|
|
53
42
|
// ctx.db is scoped to the executing user's tenant, wrong for a cross-tenant override
|
|
54
43
|
// on both the existing-check and the executor's stream reads; re-scope to the target.
|
|
55
|
-
|
|
56
|
-
const scopedDb = override !== undefined
|
|
57
|
-
? createTenantDb(db.unsafeRaw(), override, // @cast-boundary engine-bridge
|
|
58
|
-
"tenant")
|
|
59
|
-
: db;
|
|
44
|
+
const { db: scopedDb, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
60
45
|
const existing = await fetchOne(scopedDb, pagesTable, {
|
|
61
46
|
tenantId,
|
|
62
47
|
slug: event.payload.slug,
|
|
@@ -75,7 +60,7 @@ export const setWrite = defineWriteHandler({
|
|
|
75
60
|
ogImage: event.payload.ogImage !== undefined ? event.payload.ogImage : existing.ogImage,
|
|
76
61
|
published: event.payload.published !== undefined ? event.payload.published : existing.published,
|
|
77
62
|
},
|
|
78
|
-
},
|
|
63
|
+
}, event.user, scopedDb, { streamTenantId });
|
|
79
64
|
if (!result.isSuccess)
|
|
80
65
|
return result;
|
|
81
66
|
return {
|
|
@@ -92,7 +77,7 @@ export const setWrite = defineWriteHandler({
|
|
|
92
77
|
ogImage: event.payload.ogImage ?? null,
|
|
93
78
|
published: event.payload.published ?? false,
|
|
94
79
|
tenantId,
|
|
95
|
-
},
|
|
80
|
+
}, event.user, scopedDb, { streamTenantId });
|
|
96
81
|
if (!result.isSuccess)
|
|
97
82
|
return result;
|
|
98
83
|
return {
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible,
|
|
|
14
14
|
export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
|
|
15
15
|
export { rateLimitedTextResponse } from "./rate-limited-text-response.js";
|
|
16
16
|
export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
|
|
17
|
+
export { requireForTenant } from "./require-for-tenant.js";
|
|
17
18
|
export { type RowBoundGrantResult, redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
|
|
18
19
|
export { runInSubTransaction } from "./run-in-sub-transaction.js";
|
|
19
20
|
export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
|
package/dist/shared/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export { denyUnlessJoinRowParentVisible, hostRowIsGone, joinRowParentIsVisible,
|
|
|
14
14
|
export { hashPassword, verifyDummyPassword, verifyPassword } from "./password-hashing.js";
|
|
15
15
|
export { rateLimitedTextResponse } from "./rate-limited-text-response.js";
|
|
16
16
|
export { redactBotTokens, redactEmailAddresses, redactErrorText, redactUrls, } from "./redact.js";
|
|
17
|
+
export { requireForTenant } from "./require-for-tenant.js";
|
|
17
18
|
export { redeemRowBoundGrant, signRowBoundGrant, } from "./row-bound-grant.js";
|
|
18
19
|
export { runInSubTransaction } from "./run-in-sub-transaction.js";
|
|
19
20
|
export { registrationLocaleFromRequest, sessionLocaleField } from "./session-locale-field.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
|
+
// ctx.forTenant is absent for r.systemScope() handlers and without a db; a write handler that
|
|
3
|
+
// needs a cross-tenant target must fail loudly instead of falling back to ctx.db.
|
|
4
|
+
export function requireForTenant(ctx, targetTenantId) {
|
|
5
|
+
if (!ctx.forTenant) {
|
|
6
|
+
throw new InternalError({ message: "ctx.forTenant is not available in this handler context" });
|
|
7
|
+
}
|
|
8
|
+
return ctx.forTenant(targetTenantId);
|
|
9
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import {
|
|
2
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { TEXT_BLOCK_KIND } from "../constants.js";
|
|
5
5
|
import { templateResourcesTable } from "../table.js";
|
|
@@ -28,9 +28,6 @@ export const bySlugQuery = defineQueryHandler({
|
|
|
28
28
|
description: "Reads one text-block of a tenant by slug and locale together with its body and format; it is pinned to the text-block kind so mail templates and AI prompts in the same table stay unreachable through this anonymous-capable path.",
|
|
29
29
|
handler: async (query, ctx) => {
|
|
30
30
|
const override = query.payload.tenantIdOverride;
|
|
31
|
-
const overrideDenied = crossTenantOverrideDenied(query.user, override, "templateResolver.errors.tenantOverrideRequiresSystemAdmin");
|
|
32
|
-
if (overrideDenied)
|
|
33
|
-
throw overrideDenied;
|
|
34
31
|
const tenantId = override ?? query.user.tenantId;
|
|
35
32
|
const row = await fetchOne(ctx.db, templateResourcesTable, {
|
|
36
33
|
tenantId,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { castTenantRows } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import {
|
|
3
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import * as z from "zod";
|
|
5
5
|
import { TEXT_BLOCK_KIND } from "../constants.js";
|
|
6
6
|
import { templateResourcesTable } from "../table.js";
|
|
@@ -15,9 +15,6 @@ export const byTenantQuery = defineQueryHandler({
|
|
|
15
15
|
description: "Lists every text-block of a tenant with slug, locale, title and body so a public content tree can be rendered in one call; use it for the whole sidebar, and by-slug when only one block is needed.",
|
|
16
16
|
handler: async (query, ctx) => {
|
|
17
17
|
const override = query.payload.tenantIdOverride;
|
|
18
|
-
const overrideDenied = crossTenantOverrideDenied(query.user, override, "templateResolver.errors.tenantOverrideRequiresSystemAdmin");
|
|
19
|
-
if (overrideDenied)
|
|
20
|
-
throw overrideDenied;
|
|
21
18
|
const tenantId = override ?? query.user.tenantId;
|
|
22
19
|
const rows = castTenantRows(await selectMany(ctx.db, templateResourcesTable, { tenantId, kind: TEXT_BLOCK_KIND }));
|
|
23
20
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import {
|
|
2
|
+
import { defineQueryHandler, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { collectionStore, DEFAULT_COLLECTION_ACCESS, toCollectionEntry, } from "./collection-shared.js";
|
|
5
5
|
// Single entry of one collection, for the editor behind a tree node. Same
|
|
@@ -20,9 +20,6 @@ export function makeCollectionItemQuery(collection) {
|
|
|
20
20
|
: `Reads one tenant-wide "${collection.id}" entry by slug and locale, returning null when it does not exist; use it to open a single entry in the editor rather than to list the collection.`,
|
|
21
21
|
handler: async (query, ctx) => {
|
|
22
22
|
const override = query.payload.tenantIdOverride;
|
|
23
|
-
const overrideDenied = crossTenantOverrideDenied(query.user, override, "templateResolver.errors.tenantOverrideRequiresSystemAdmin");
|
|
24
|
-
if (overrideDenied)
|
|
25
|
-
throw overrideDenied;
|
|
26
23
|
const tenantId = override ?? query.user.tenantId;
|
|
27
24
|
const row = await fetchOne(ctx.db, store.table, {
|
|
28
25
|
tenantId,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { castTenantRows } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import {
|
|
3
|
+
import { defineQueryHandler, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import * as z from "zod";
|
|
5
5
|
import { collectionStore, DEFAULT_COLLECTION_ACCESS, toCollectionEntry, } from "./collection-shared.js";
|
|
6
6
|
// One list handler per declared collection: `kind`, `ownership` and `access`
|
|
@@ -25,9 +25,6 @@ export function makeCollectionListQuery(collection) {
|
|
|
25
25
|
: `Lists the tenant-wide entries of the "${collection.id}" content collection (${collection.kind} content) with slug, locale, title and body; every caller who may reach this collection sees the same shared set.`,
|
|
26
26
|
handler: async (query, ctx) => {
|
|
27
27
|
const override = query.payload.tenantIdOverride;
|
|
28
|
-
const overrideDenied = crossTenantOverrideDenied(query.user, override, "templateResolver.errors.tenantOverrideRequiresSystemAdmin");
|
|
29
|
-
if (overrideDenied)
|
|
30
|
-
throw overrideDenied;
|
|
31
28
|
const tenantId = override ?? query.user.tenantId;
|
|
32
29
|
const rows = castTenantRows(await selectMany(ctx.db, store.table, {
|
|
33
30
|
tenantId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import {
|
|
3
|
-
import { writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
|
+
import { defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
3
|
import * as z from "zod";
|
|
4
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
5
5
|
import { collectionStore, DEFAULT_COLLECTION_ACCESS, } from "./collection-shared.js";
|
|
6
6
|
import { contentFormatSchema, folderSchema, localeSchema, slugSchema } from "./shared.js";
|
|
7
7
|
// Upsert inside one collection. Mirrors set.write for text-blocks, but the
|
|
@@ -30,15 +30,11 @@ export function makeCollectionSetWrite(collection) {
|
|
|
30
30
|
? `Creates or overwrites the caller's own "${collection.id}" entry at a slug and locale, live on save with no draft stage; it always writes the acting user's entry, never another user's.`
|
|
31
31
|
: `Creates or overwrites the tenant-wide "${collection.id}" entry at a slug and locale, live on save with no draft stage; the change is visible to everyone who may reach this shared collection.`,
|
|
32
32
|
handler: async (event, ctx) => {
|
|
33
|
-
const db = ctx.db;
|
|
34
33
|
const override = event.payload.tenantIdOverride;
|
|
35
|
-
const overrideDenied = crossTenantOverrideDenied(event.user, override, "templateResolver.errors.tenantOverrideRequiresSystemAdmin");
|
|
36
|
-
if (overrideDenied)
|
|
37
|
-
return writeFailure(overrideDenied);
|
|
38
34
|
// @cast-boundary engine-payload — override is a zod-validated string, the
|
|
39
35
|
// user's own tenantId is already TenantId-branded.
|
|
40
36
|
const tenantId = (override ?? event.user.tenantId);
|
|
41
|
-
const
|
|
37
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
42
38
|
// Scoped to the acting user even under a tenant override: a SystemAdmin
|
|
43
39
|
// writing into another tenant still writes their own entry, never
|
|
44
40
|
// someone else's signature.
|
|
@@ -63,7 +59,7 @@ export function makeCollectionSetWrite(collection) {
|
|
|
63
59
|
contentFormat: event.payload.contentFormat,
|
|
64
60
|
folder: event.payload.folder ?? null,
|
|
65
61
|
},
|
|
66
|
-
},
|
|
62
|
+
}, event.user, db, { streamTenantId });
|
|
67
63
|
if (!result.isSuccess)
|
|
68
64
|
return result;
|
|
69
65
|
return {
|
|
@@ -82,7 +78,7 @@ export function makeCollectionSetWrite(collection) {
|
|
|
82
78
|
folder: event.payload.folder ?? null,
|
|
83
79
|
...store.createDefaults(tenantId),
|
|
84
80
|
...owner,
|
|
85
|
-
},
|
|
81
|
+
}, event.user, db, { streamTenantId });
|
|
86
82
|
if (!result.isSuccess)
|
|
87
83
|
return result;
|
|
88
84
|
return {
|