@cosmicdrift/kumiko-samples 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/package.json +1 -1
- package/packages/bundled-features/package.json +1 -1
- package/packages/bundled-features/src/agent-tools/agent-manifest.ts +2 -0
- package/packages/bundled-features/src/agent-tools/types.ts +2 -0
- package/packages/bundled-features/src/audit/escape-hatch-audit-sink.ts +2 -0
- package/packages/bundled-features/src/audit/handlers/resolve-audit-tenant.ts +8 -7
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +7 -12
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +13 -17
- package/packages/bundled-features/src/cap-overview/handlers/caps-usage.query.ts +12 -8
- package/packages/bundled-features/src/compliance-profiles/handlers/set-profile.write.ts +19 -36
- package/packages/bundled-features/src/custom-fields/handlers/define-system-field.write.ts +10 -17
- package/packages/bundled-features/src/custom-fields/handlers/delete-system-field.write.ts +5 -2
- package/packages/bundled-features/src/custom-fields/lib/define-or-resurrect.ts +12 -3
- package/packages/bundled-features/src/delivery/delivery-service.ts +1 -1
- package/packages/bundled-features/src/managed-pages/handlers/by-tenant-published.query.ts +1 -10
- package/packages/bundled-features/src/managed-pages/handlers/set.write.ts +9 -37
- package/packages/bundled-features/src/shared/index.ts +1 -0
- package/packages/bundled-features/src/shared/require-for-tenant.ts +18 -0
- package/packages/bundled-features/src/template-resolver/handlers/by-slug.query.ts +1 -10
- package/packages/bundled-features/src/template-resolver/handlers/by-tenant.query.ts +1 -10
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +0 -7
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +0 -7
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +6 -12
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +7 -15
- package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +7 -6
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +7 -11
- package/packages/bundled-features/src/template-resolver/seeding.ts +1 -2
- package/packages/bundled-features/src/tenant/cancel-pending-invitation.ts +3 -1
- package/packages/bundled-features/src/tenant/handlers/remove-member.write.ts +22 -8
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +19 -11
- package/packages/bundled-features/src/tenant-lifecycle/feature.ts +1 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -0
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +3 -0
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +9 -23
- package/packages/bundled-features/src/user-data-rights/feature.ts +2 -0
- package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +5 -0
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +11 -1
- package/packages/bundled-features/src/user-data-rights/run-user-export.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-samples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.356.0",
|
|
4
4
|
"description": "Source trees of the Kumiko sample recipes, sample apps and bundled features in repo layout, for tooling such as few-shot corpus builds.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.356.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -251,6 +251,7 @@ function buildHandlerEntry(
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
const entity = getHandlerEntity(qn);
|
|
254
|
+
const excludedFields = "excludedFields" in def ? def.excludedFields : undefined;
|
|
254
255
|
return {
|
|
255
256
|
qn,
|
|
256
257
|
kind,
|
|
@@ -258,6 +259,7 @@ function buildHandlerEntry(
|
|
|
258
259
|
risk: exposure.risk,
|
|
259
260
|
inputSchema,
|
|
260
261
|
...(entity !== undefined && { entity }),
|
|
262
|
+
...(excludedFields !== undefined && { excludedFields }),
|
|
261
263
|
};
|
|
262
264
|
}
|
|
263
265
|
|
|
@@ -152,6 +152,8 @@ export type AgentManifestHandler = {
|
|
|
152
152
|
readonly risk: AgentRisk;
|
|
153
153
|
readonly inputSchema: Readonly<Record<string, unknown>>;
|
|
154
154
|
readonly entity?: string;
|
|
155
|
+
/** Fields this generic create/update handler never writes; sending one fails validation. */
|
|
156
|
+
readonly excludedFields?: readonly string[];
|
|
155
157
|
};
|
|
156
158
|
|
|
157
159
|
export type AgentManifestScreen = {
|
|
@@ -23,6 +23,7 @@ export const escapeHatchUsedSchema = z.object({
|
|
|
23
23
|
kind: z.enum(escapeHatchKinds),
|
|
24
24
|
reason: z.string(),
|
|
25
25
|
actor: z.string(),
|
|
26
|
+
caller: z.string().optional(),
|
|
26
27
|
targetUserId: z.string().optional(),
|
|
27
28
|
targetTenantId: z.string().optional(),
|
|
28
29
|
});
|
|
@@ -36,6 +37,7 @@ export function createEscapeHatchAuditSink(opts: {
|
|
|
36
37
|
kind: event.kind,
|
|
37
38
|
reason: event.reason,
|
|
38
39
|
actor: event.actor,
|
|
40
|
+
...(event.caller !== undefined && { caller: event.caller }),
|
|
39
41
|
...(event.target && { targetUserId: event.target.id, targetTenantId: event.target.tenantId }),
|
|
40
42
|
});
|
|
41
43
|
// Appends on the unbound pool, never the handler tx — an audit entry must
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mayOverrideTenant, type SessionUser } 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 { type AuditScope, AuditScopes } from "../constants.js";
|
|
@@ -12,11 +13,11 @@ export function resolveAuditScopeFilter(
|
|
|
12
13
|
scope: AuditScope | undefined,
|
|
13
14
|
): AuditScopeFilter {
|
|
14
15
|
if (scope !== AuditScopes.system) return { tenantId: user.tenantId };
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
if (!mayOverrideTenant(user)) {
|
|
17
|
+
throw new AccessDeniedError({
|
|
18
|
+
i18nKey: "audit.errors.systemScopeRequiresSystemAdmin",
|
|
19
|
+
details: { reason: FrameworkReasons.tenantOverrideRequiresSystemAdmin },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
21
22
|
return { tenantId: SYSTEM_TENANT_ID, aggregateType: APP_INSTANCE_STREAM_TYPE };
|
|
22
23
|
}
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
2
|
import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id.js";
|
|
4
3
|
import { SubscriptionFoundationHandlers, SubscriptionStatuses } from "../constants.js";
|
|
5
4
|
import {
|
|
@@ -75,17 +74,13 @@ export function registerConsumerProtection(
|
|
|
75
74
|
// user), never from payload data.
|
|
76
75
|
const tenantId = ctx.systemUser.tenantId;
|
|
77
76
|
const currentPeriodEndIso = payload["currentPeriodEndIso"];
|
|
78
|
-
const result = await ctx.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
: paymentAggregateId(tenantId),
|
|
86
|
-
...(typeof currentPeriodEndIso === "string" && { currentPeriodEndIso }),
|
|
87
|
-
},
|
|
88
|
-
);
|
|
77
|
+
const result = await ctx.write(SubscriptionFoundationHandlers.issueContractConfirmation, {
|
|
78
|
+
consentId,
|
|
79
|
+
sourceAggregateId: isSubscriptionEvent
|
|
80
|
+
? subscriptionAggregateId(tenantId)
|
|
81
|
+
: paymentAggregateId(tenantId),
|
|
82
|
+
...(typeof currentPeriodEndIso === "string" && { currentPeriodEndIso }),
|
|
83
|
+
});
|
|
89
84
|
if (!result.isSuccess) {
|
|
90
85
|
throw new Error(
|
|
91
86
|
`billing-foundation:${trigger.jobName}: issue-contract-confirmation failed: ${JSON.stringify(result.error)}`,
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { SubscriptionCancelTimings, SubscriptionFoundationHandlers } from "../constants.js";
|
|
3
3
|
import {
|
|
4
4
|
CONTRACT_TERMINATION_DECLARED_EVENT_QN,
|
|
@@ -59,22 +59,18 @@ export function registerContractTermination(
|
|
|
59
59
|
const declared = contractTerminationDeclaredPayloadSchema.safeParse(payload);
|
|
60
60
|
// skip: trigger event carries no usable declaration
|
|
61
61
|
if (!declared.success) return;
|
|
62
|
-
const result = await ctx.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
receivedAtIso: declared.data.receivedAtIso,
|
|
75
|
-
locale: declared.data.locale,
|
|
76
|
-
},
|
|
77
|
-
);
|
|
62
|
+
const result = await ctx.write(SubscriptionFoundationHandlers.recordContractTermination, {
|
|
63
|
+
requestId: declared.data.requestId,
|
|
64
|
+
declarationType: declared.data.declarationType,
|
|
65
|
+
terminationKind: declared.data.terminationKind,
|
|
66
|
+
channel: "public",
|
|
67
|
+
when:
|
|
68
|
+
declared.data.declarationType === "withdrawal"
|
|
69
|
+
? "none"
|
|
70
|
+
: SubscriptionCancelTimings.periodEnd,
|
|
71
|
+
receivedAtIso: declared.data.receivedAtIso,
|
|
72
|
+
locale: declared.data.locale,
|
|
73
|
+
});
|
|
78
74
|
if (!result.isSuccess) {
|
|
79
75
|
throw new Error(
|
|
80
76
|
`billing-foundation:cancel-on-public-termination-declared: record-contract-termination failed: ${JSON.stringify(result.error)}`,
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import {
|
|
4
|
-
crossTenantOverrideDenied,
|
|
5
4
|
defineQueryHandler,
|
|
5
|
+
mayOverrideTenant,
|
|
6
6
|
type QueryHandlerDefinition,
|
|
7
7
|
type TenantId,
|
|
8
8
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
AccessDeniedError,
|
|
11
|
+
FrameworkReasons,
|
|
12
|
+
InternalError,
|
|
13
|
+
} from "@cosmicdrift/kumiko-framework/errors";
|
|
10
14
|
import * as z from "zod";
|
|
11
15
|
import { tierAssignmentEntity } from "../../tier-engine/index.js";
|
|
12
16
|
import type { CapSpec, CapUsageWithMeta } from "../types.js";
|
|
@@ -39,12 +43,12 @@ export function createCapsUsageQuery(
|
|
|
39
43
|
});
|
|
40
44
|
}
|
|
41
45
|
const override = query.payload.tenantId;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
if (override !== undefined && !mayOverrideTenant(query.user)) {
|
|
47
|
+
throw new AccessDeniedError({
|
|
48
|
+
i18nKey: "cap-overview.errors.tenantOverrideRequiresSystemAdmin",
|
|
49
|
+
details: { reason: FrameworkReasons.tenantOverrideRequiresSystemAdmin },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
48
52
|
|
|
49
53
|
const targetTenantId = override ?? query.user.tenantId;
|
|
50
54
|
// Both branches return the SAME unfiltered system-mode db —
|
|
@@ -4,18 +4,14 @@ import {
|
|
|
4
4
|
SELECTABLE_PROFILE_KEYS,
|
|
5
5
|
} from "@cosmicdrift/kumiko-framework/compliance";
|
|
6
6
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
7
|
-
import {
|
|
8
|
-
access,
|
|
9
|
-
crossTenantOverrideDenied,
|
|
10
|
-
defineWriteHandler,
|
|
11
|
-
type TenantId,
|
|
12
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import { access, defineWriteHandler, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
13
8
|
import {
|
|
14
9
|
UnprocessableError,
|
|
15
10
|
validationErrorFromZod,
|
|
16
11
|
writeFailure,
|
|
17
12
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
18
13
|
import * as z from "zod";
|
|
14
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
19
15
|
import {
|
|
20
16
|
tenantComplianceProfileEntity,
|
|
21
17
|
tenantComplianceProfileTable,
|
|
@@ -31,24 +27,16 @@ const crud = createEventStoreExecutor(tenantComplianceProfileTable, tenantCompli
|
|
|
31
27
|
// SELECTABLE_PROFILE_KEYS aus der framework/compliance-Liste.
|
|
32
28
|
const profileKeySchema = z.enum(SELECTABLE_PROFILE_KEYS);
|
|
33
29
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// Upsert-Verhalten: erste Wahl insert, weitere update. Idempotent —
|
|
37
|
-
// wer mit gleichen Werten zweimal aufruft, kriegt das gleiche Ergebnis
|
|
38
|
-
// (modulo aktualisierte Audit-Events im Event-Store).
|
|
30
|
+
// Upsert: the first call inserts, later calls update, so repeating the same values
|
|
31
|
+
// is idempotent apart from the extra audit events.
|
|
39
32
|
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// Stream-Lookup nicht miss → version_conflict gibt (Memory:
|
|
45
|
-
// feedback_event_store_tenant_consistency).
|
|
33
|
+
// A SystemAdmin may target another tenant via `tenantIdOverride` (operator setup,
|
|
34
|
+
// onboarding migrations); the dispatcher rejects the override for anyone else. The
|
|
35
|
+
// executor then writes into the target tenant's stream via streamTenantId while
|
|
36
|
+
// the operator stays the recorded actor.
|
|
46
37
|
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// - override (optional) muss valides JSON-Object sein
|
|
50
|
-
// - override Top-Level-Keys muessen in ALLOWED_OVERRIDE_KEYS sein
|
|
51
|
-
// — verhindert Tippfehler die deepMerge stillschweigend ignoriert
|
|
38
|
+
// Override top-level keys are restricted to ALLOWED_OVERRIDE_KEYS because deepMerge
|
|
39
|
+
// would silently ignore a typo.
|
|
52
40
|
export const setProfileWrite = defineWriteHandler({
|
|
53
41
|
name: "set-profile",
|
|
54
42
|
schema: z.object({
|
|
@@ -62,15 +50,8 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
62
50
|
description:
|
|
63
51
|
"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.",
|
|
64
52
|
handler: async (event, ctx) => {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
event.user,
|
|
68
|
-
tenantOverride,
|
|
69
|
-
"complianceProfiles.errors.tenantOverrideRequiresSystemAdmin",
|
|
70
|
-
);
|
|
71
|
-
if (overrideDenied) return writeFailure(overrideDenied);
|
|
72
|
-
const tenantId = (tenantOverride ?? event.user.tenantId) as TenantId; // @cast-boundary engine-payload
|
|
73
|
-
const executorUser = tenantOverride !== undefined ? { ...event.user, tenantId } : event.user;
|
|
53
|
+
const tenantId = (event.payload.tenantIdOverride ?? event.user.tenantId) as TenantId; // @cast-boundary engine-payload
|
|
54
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
74
55
|
|
|
75
56
|
// Override-Validation: muss parseables JSON-Object sein UND dem
|
|
76
57
|
// ComplianceProfileOverride-Schema entsprechen (S1.9 Z3 — strict-Zod
|
|
@@ -100,7 +81,7 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
100
81
|
}
|
|
101
82
|
|
|
102
83
|
// Upsert: existierenden Eintrag suchen
|
|
103
|
-
const existing = (await fetchOne(
|
|
84
|
+
const existing = (await fetchOne(db, tenantComplianceProfileTable, {
|
|
104
85
|
tenantId: tenantId,
|
|
105
86
|
})) as { id: string; version: number } | null; // @cast-boundary db-runner
|
|
106
87
|
|
|
@@ -114,8 +95,9 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
114
95
|
override: event.payload.override ?? null,
|
|
115
96
|
},
|
|
116
97
|
},
|
|
117
|
-
|
|
118
|
-
|
|
98
|
+
event.user,
|
|
99
|
+
db,
|
|
100
|
+
{ streamTenantId },
|
|
119
101
|
);
|
|
120
102
|
if (!result.isSuccess) return result;
|
|
121
103
|
return {
|
|
@@ -130,8 +112,9 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
130
112
|
override: event.payload.override ?? null,
|
|
131
113
|
tenantId,
|
|
132
114
|
},
|
|
133
|
-
|
|
134
|
-
|
|
115
|
+
event.user,
|
|
116
|
+
db,
|
|
117
|
+
{ streamTenantId },
|
|
135
118
|
);
|
|
136
119
|
if (!result.isSuccess) return result;
|
|
137
120
|
return {
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
import { SYSTEM_TENANT_ID, type WriteHandlerDef } 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 { type DefineFieldPayload, defineFieldPayloadSchema } from "../schemas.js";
|
|
6
7
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// **Use-Case:** Vendor (cdgs) sagt "alle Hausverwaltungs-customers haben ab
|
|
13
|
-
// heute ein `internalNumber`-Field". Tenant kann den Wert pro customer
|
|
14
|
-
// setzen, aber die Definition nicht ändern oder löschen.
|
|
15
|
-
//
|
|
16
|
-
// **Same-scope-conflict** wie bei define-tenant-field via aggregate-version-
|
|
17
|
-
// conflict (deterministische ID mit tenantId=SYSTEM_TENANT_ID).
|
|
8
|
+
// A system-wide field definition applies to every tenant. tenantId is forced to
|
|
9
|
+
// SYSTEM_TENANT_ID (never taken from the caller) and addressed via streamTenantId,
|
|
10
|
+
// so the SystemAdmin stays the recorded actor. Tenants can set values for the field
|
|
11
|
+
// but cannot change or delete the definition. A same-scope duplicate surfaces as an
|
|
12
|
+
// aggregate version conflict, because the ID is derived from SYSTEM_TENANT_ID.
|
|
18
13
|
export const defineSystemFieldHandler: WriteHandlerDef = {
|
|
19
14
|
name: "define-system-field",
|
|
20
15
|
schema: defineFieldPayloadSchema,
|
|
@@ -30,16 +25,14 @@ export const defineSystemFieldHandler: WriteHandlerDef = {
|
|
|
30
25
|
payload.fieldKey,
|
|
31
26
|
);
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
// write. The framework's CrudExecutor writes the row with this tenantId
|
|
35
|
-
// — the row lives in the system-scope-stream.
|
|
36
|
-
const systemUser = { ...event.user, tenantId: SYSTEM_TENANT_ID };
|
|
28
|
+
const { db, streamTenantId } = requireForTenant(ctx, SYSTEM_TENANT_ID);
|
|
37
29
|
|
|
38
30
|
return defineOrResurrectFieldDefinition(
|
|
39
31
|
aggregateId,
|
|
40
32
|
buildFieldDefinitionColumns(payload),
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
event.user,
|
|
34
|
+
db,
|
|
35
|
+
streamTenantId,
|
|
43
36
|
);
|
|
44
37
|
},
|
|
45
38
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SYSTEM_TENANT_ID, type WriteHandlerDef } 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";
|
|
@@ -26,8 +27,10 @@ export const deleteSystemFieldHandler: WriteHandlerDef = {
|
|
|
26
27
|
payload.fieldKey,
|
|
27
28
|
);
|
|
28
29
|
|
|
29
|
-
const
|
|
30
|
-
const result = await fieldDefinitionExecutor.delete({ id: aggregateId },
|
|
30
|
+
const { db, streamTenantId } = requireForTenant(ctx, SYSTEM_TENANT_ID);
|
|
31
|
+
const result = await fieldDefinitionExecutor.delete({ id: aggregateId }, event.user, db, {
|
|
32
|
+
streamTenantId,
|
|
33
|
+
});
|
|
31
34
|
|
|
32
35
|
// Cascade-cleanup-Event — host-entity-MSPs entfernen orphan values aus
|
|
33
36
|
// ihrer customFields jsonb. Im selben TX = atomic.
|
|
@@ -1,3 +1,4 @@
|
|
|
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
|
|
|
@@ -27,13 +28,18 @@ export async function defineOrResurrectFieldDefinition(
|
|
|
27
28
|
columns: FieldDefinitionColumns,
|
|
28
29
|
user: DefineUser,
|
|
29
30
|
db: DefineDb,
|
|
31
|
+
streamTenantId?: TenantId,
|
|
30
32
|
): Promise<WriteResult> {
|
|
31
33
|
const active = await fieldDefinitionExecutor.detail({ id: aggregateId }, user, db);
|
|
32
34
|
if (active) {
|
|
33
|
-
return fieldDefinitionExecutor.create({ id: aggregateId, ...columns }, user, db
|
|
35
|
+
return fieldDefinitionExecutor.create({ id: aggregateId, ...columns }, user, db, {
|
|
36
|
+
streamTenantId,
|
|
37
|
+
});
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
const restored = await fieldDefinitionExecutor.restore({ id: aggregateId }, user, db
|
|
40
|
+
const restored = await fieldDefinitionExecutor.restore({ id: aggregateId }, user, db, {
|
|
41
|
+
streamTenantId,
|
|
42
|
+
});
|
|
37
43
|
if (restored.isSuccess) {
|
|
38
44
|
// restore() just un-deleted the row in this same tx; no concurrent writer
|
|
39
45
|
// exists, so skip the optimistic-lock version match (we'd otherwise have to
|
|
@@ -43,10 +49,13 @@ export async function defineOrResurrectFieldDefinition(
|
|
|
43
49
|
// readonly FieldDefinitionColumns isn't assignable to it.
|
|
44
50
|
return fieldDefinitionExecutor.update({ id: aggregateId, changes: { ...columns } }, user, db, {
|
|
45
51
|
skipOptimisticLock: true,
|
|
52
|
+
streamTenantId,
|
|
46
53
|
});
|
|
47
54
|
}
|
|
48
55
|
if (restored.error.code === "not_found") {
|
|
49
|
-
return fieldDefinitionExecutor.create({ id: aggregateId, ...columns }, user, db
|
|
56
|
+
return fieldDefinitionExecutor.create({ id: aggregateId, ...columns }, user, db, {
|
|
57
|
+
streamTenantId,
|
|
58
|
+
});
|
|
50
59
|
}
|
|
51
60
|
return restored;
|
|
52
61
|
}
|
|
@@ -190,7 +190,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
190
190
|
tenantId,
|
|
191
191
|
actor: systemUser.id,
|
|
192
192
|
sink: escapeHatchAuditSink,
|
|
193
|
-
log,
|
|
193
|
+
log: logError,
|
|
194
194
|
});
|
|
195
195
|
// ctx.db/dbOutsideTransaction are tenant-filtered even for a systemScope handler:
|
|
196
196
|
// the dispatcher closes them off entirely there, so "system" mode would hand this
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { castTenantRows } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import {
|
|
4
|
-
crossTenantOverrideDenied,
|
|
5
|
-
defineQueryHandler,
|
|
6
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
4
|
import * as z from "zod";
|
|
8
5
|
import { type PageRow, pagesTable } from "../table.js";
|
|
9
6
|
|
|
@@ -33,12 +30,6 @@ export const byTenantPublishedQuery = defineQueryHandler({
|
|
|
33
30
|
"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.",
|
|
34
31
|
handler: async (query, ctx) => {
|
|
35
32
|
const override = query.payload.tenantIdOverride;
|
|
36
|
-
const overrideDenied = crossTenantOverrideDenied(
|
|
37
|
-
query.user,
|
|
38
|
-
override,
|
|
39
|
-
"managedPages.errors.tenantOverrideRequiresSystemAdmin",
|
|
40
|
-
);
|
|
41
|
-
if (overrideDenied) throw overrideDenied;
|
|
42
33
|
const tenantId = override ?? query.user.tenantId;
|
|
43
34
|
const rows = castTenantRows<PageRow>(
|
|
44
35
|
await selectMany(ctx.db, pagesTable, { tenantId: tenantId, published: true }),
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import { createEventStoreExecutor
|
|
3
|
-
import {
|
|
4
|
-
crossTenantOverrideDenied,
|
|
5
|
-
defineWriteHandler,
|
|
6
|
-
type TenantId,
|
|
7
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
-
import { writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
|
+
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import { defineWriteHandler, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
4
|
import * as z from "zod";
|
|
5
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
10
6
|
import { type PageRow, pageEntity, pagesTable } from "../table.js";
|
|
11
7
|
|
|
12
8
|
const slugSchema = z
|
|
@@ -23,9 +19,6 @@ const langSchema = z
|
|
|
23
19
|
|
|
24
20
|
const executor = createEventStoreExecutor(pagesTable, pageEntity, { entityName: "page" });
|
|
25
21
|
|
|
26
|
-
const SET_PAGE_TENANT_OVERRIDE_REASON =
|
|
27
|
-
"SystemAdmin tenantIdOverride re-scopes the page write to the target tenant";
|
|
28
|
-
|
|
29
22
|
// Upsert einer Page — eine Operation pro (tenantId, slug, lang). Tenant-
|
|
30
23
|
// Scope default aus event.user; SystemAdmin kann via `tenantIdOverride`
|
|
31
24
|
// für einen anderen Tenant schreiben (typisch SYSTEM_TENANT_ID für app-
|
|
@@ -48,35 +41,12 @@ export const setWrite = defineWriteHandler({
|
|
|
48
41
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
49
42
|
description:
|
|
50
43
|
"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.",
|
|
51
|
-
escapeHatch: {
|
|
52
|
-
grants: ["unsafeRaw"],
|
|
53
|
-
reason: SET_PAGE_TENANT_OVERRIDE_REASON,
|
|
54
|
-
},
|
|
55
44
|
handler: async (event, ctx) => {
|
|
56
|
-
const db = ctx.db;
|
|
57
45
|
const override = event.payload.tenantIdOverride;
|
|
58
|
-
const
|
|
59
|
-
event.user,
|
|
60
|
-
override,
|
|
61
|
-
"managedPages.errors.tenantOverrideRequiresSystemAdmin",
|
|
62
|
-
);
|
|
63
|
-
if (overrideDenied) return writeFailure(overrideDenied);
|
|
64
|
-
const tenantId = override ?? event.user.tenantId;
|
|
65
|
-
// override: point the executor context at the target tenant, else getStreamVersion runs against user.tenantId → version_conflict.
|
|
66
|
-
const executorUser =
|
|
67
|
-
override !== undefined ? { ...event.user, tenantId: override as TenantId } : event.user; // @cast-boundary engine-bridge
|
|
68
|
-
|
|
46
|
+
const tenantId = (override ?? event.user.tenantId) as TenantId; // @cast-boundary engine-bridge
|
|
69
47
|
// ctx.db is scoped to the executing user's tenant, wrong for a cross-tenant override
|
|
70
48
|
// on both the existing-check and the executor's stream reads; re-scope to the target.
|
|
71
|
-
|
|
72
|
-
const scopedDb =
|
|
73
|
-
override !== undefined
|
|
74
|
-
? createTenantDb(
|
|
75
|
-
db.unsafeRaw(),
|
|
76
|
-
override as TenantId, // @cast-boundary engine-bridge
|
|
77
|
-
"tenant",
|
|
78
|
-
)
|
|
79
|
-
: db;
|
|
49
|
+
const { db: scopedDb, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
80
50
|
const existing = await fetchOne<PageRow>(scopedDb, pagesTable, {
|
|
81
51
|
tenantId,
|
|
82
52
|
slug: event.payload.slug,
|
|
@@ -100,8 +70,9 @@ export const setWrite = defineWriteHandler({
|
|
|
100
70
|
event.payload.published !== undefined ? event.payload.published : existing.published,
|
|
101
71
|
},
|
|
102
72
|
},
|
|
103
|
-
|
|
73
|
+
event.user,
|
|
104
74
|
scopedDb,
|
|
75
|
+
{ streamTenantId },
|
|
105
76
|
);
|
|
106
77
|
if (!result.isSuccess) return result;
|
|
107
78
|
return {
|
|
@@ -121,8 +92,9 @@ export const setWrite = defineWriteHandler({
|
|
|
121
92
|
published: event.payload.published ?? false,
|
|
122
93
|
tenantId,
|
|
123
94
|
},
|
|
124
|
-
|
|
95
|
+
event.user,
|
|
125
96
|
scopedDb,
|
|
97
|
+
{ streamTenantId },
|
|
126
98
|
);
|
|
127
99
|
if (!result.isSuccess) return result;
|
|
128
100
|
return {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HandlerContext,
|
|
3
|
+
TenantId,
|
|
4
|
+
TenantWriteTarget,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
7
|
+
|
|
8
|
+
// ctx.forTenant is absent for r.systemScope() handlers and without a db; a write handler that
|
|
9
|
+
// needs a cross-tenant target must fail loudly instead of falling back to ctx.db.
|
|
10
|
+
export function requireForTenant(
|
|
11
|
+
ctx: Pick<HandlerContext, "forTenant">,
|
|
12
|
+
targetTenantId: TenantId,
|
|
13
|
+
): TenantWriteTarget {
|
|
14
|
+
if (!ctx.forTenant) {
|
|
15
|
+
throw new InternalError({ message: "ctx.forTenant is not available in this handler context" });
|
|
16
|
+
}
|
|
17
|
+
return ctx.forTenant(targetTenantId);
|
|
18
|
+
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import {
|
|
3
|
-
crossTenantOverrideDenied,
|
|
4
|
-
defineQueryHandler,
|
|
5
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
3
|
import * as z from "zod";
|
|
7
4
|
import { TEXT_BLOCK_KIND } from "../constants.js";
|
|
8
5
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
@@ -33,12 +30,6 @@ export const bySlugQuery = defineQueryHandler({
|
|
|
33
30
|
"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.",
|
|
34
31
|
handler: async (query, ctx) => {
|
|
35
32
|
const override = query.payload.tenantIdOverride;
|
|
36
|
-
const overrideDenied = crossTenantOverrideDenied(
|
|
37
|
-
query.user,
|
|
38
|
-
override,
|
|
39
|
-
"templateResolver.errors.tenantOverrideRequiresSystemAdmin",
|
|
40
|
-
);
|
|
41
|
-
if (overrideDenied) throw overrideDenied;
|
|
42
33
|
const tenantId = override ?? query.user.tenantId;
|
|
43
34
|
const row = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
|
|
44
35
|
tenantId,
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { castTenantRows } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import {
|
|
4
|
-
crossTenantOverrideDenied,
|
|
5
|
-
defineQueryHandler,
|
|
6
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
4
|
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
8
5
|
import * as z from "zod";
|
|
9
6
|
import { TEXT_BLOCK_KIND } from "../constants.js";
|
|
@@ -39,12 +36,6 @@ export const byTenantQuery = defineQueryHandler({
|
|
|
39
36
|
"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.",
|
|
40
37
|
handler: async (query, ctx) => {
|
|
41
38
|
const override = query.payload.tenantIdOverride;
|
|
42
|
-
const overrideDenied = crossTenantOverrideDenied(
|
|
43
|
-
query.user,
|
|
44
|
-
override,
|
|
45
|
-
"templateResolver.errors.tenantOverrideRequiresSystemAdmin",
|
|
46
|
-
);
|
|
47
|
-
if (overrideDenied) throw overrideDenied;
|
|
48
39
|
const tenantId = override ?? query.user.tenantId;
|
|
49
40
|
const rows = castTenantRows<TemplateResourceRow>(
|
|
50
41
|
await selectMany(ctx.db, templateResourcesTable, { tenantId, kind: TEXT_BLOCK_KIND }),
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import {
|
|
3
3
|
type ContentCollectionDefinition,
|
|
4
|
-
crossTenantOverrideDenied,
|
|
5
4
|
defineQueryHandler,
|
|
6
5
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
6
|
import * as z from "zod";
|
|
@@ -31,12 +30,6 @@ export function makeCollectionItemQuery(collection: ContentCollectionDefinition)
|
|
|
31
30
|
: `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.`,
|
|
32
31
|
handler: async (query, ctx) => {
|
|
33
32
|
const override = query.payload.tenantIdOverride;
|
|
34
|
-
const overrideDenied = crossTenantOverrideDenied(
|
|
35
|
-
query.user,
|
|
36
|
-
override,
|
|
37
|
-
"templateResolver.errors.tenantOverrideRequiresSystemAdmin",
|
|
38
|
-
);
|
|
39
|
-
if (overrideDenied) throw overrideDenied;
|
|
40
33
|
const tenantId = override ?? query.user.tenantId;
|
|
41
34
|
const row = await fetchOne<CollectionEntryRow>(ctx.db, store.table, {
|
|
42
35
|
tenantId,
|