@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
|
@@ -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, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
3
|
import * as z from "zod";
|
|
4
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
5
5
|
import { TEMPLATE_KINDS, TEXT_BLOCK_KIND } from "../constants.js";
|
|
6
6
|
import { templateResourcesTable } from "../table.js";
|
|
7
7
|
import { contentFormatSchema, executor, folderSchema, localeSchema, slugSchema } from "./shared.js";
|
|
@@ -43,18 +43,11 @@ export const setWrite = defineWriteHandler({
|
|
|
43
43
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
44
44
|
description: "Creates or overwrites one content-tree resource addressed by slug, kind and locale, carrying a title and folder and going live on save with no draft stage; a SystemAdmin may target the system tenant, where shared legal and marketing copy lives.",
|
|
45
45
|
handler: async (event, ctx) => {
|
|
46
|
-
const db = ctx.db;
|
|
47
46
|
const override = event.payload.tenantIdOverride;
|
|
48
|
-
const overrideDenied = crossTenantOverrideDenied(event.user, override, "templateResolver.errors.tenantOverrideRequiresSystemAdmin");
|
|
49
|
-
if (overrideDenied)
|
|
50
|
-
return writeFailure(overrideDenied);
|
|
51
47
|
// @cast-boundary engine-payload — override is a zod-validated string, the
|
|
52
48
|
// user's own tenantId is already TenantId-branded.
|
|
53
49
|
const tenantId = (override ?? event.user.tenantId);
|
|
54
|
-
|
|
55
|
-
// otherwise the event-store stream lookup runs against event.user.tenantId
|
|
56
|
-
// and reports a version conflict although the projection row exists.
|
|
57
|
-
const executorUser = override !== undefined ? { ...event.user, tenantId } : event.user;
|
|
50
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
58
51
|
const kind = event.payload.kind ?? TEXT_BLOCK_KIND;
|
|
59
52
|
const existing = await fetchOne(db, templateResourcesTable, {
|
|
60
53
|
tenantId,
|
|
@@ -91,7 +84,7 @@ export const setWrite = defineWriteHandler({
|
|
|
91
84
|
contentFormat: fields.contentFormat,
|
|
92
85
|
folder: fields.folder,
|
|
93
86
|
},
|
|
94
|
-
},
|
|
87
|
+
}, event.user, db, { streamTenantId });
|
|
95
88
|
if (!result.isSuccess)
|
|
96
89
|
return result;
|
|
97
90
|
return {
|
|
@@ -99,7 +92,9 @@ export const setWrite = defineWriteHandler({
|
|
|
99
92
|
data: { slug: event.payload.slug, locale: event.payload.locale, isNew: false },
|
|
100
93
|
};
|
|
101
94
|
}
|
|
102
|
-
const result = await executor.create({ ...fields, tenantId },
|
|
95
|
+
const result = await executor.create({ ...fields, tenantId }, event.user, db, {
|
|
96
|
+
streamTenantId,
|
|
97
|
+
});
|
|
103
98
|
if (!result.isSuccess)
|
|
104
99
|
return result;
|
|
105
100
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
3
4
|
import { templateResourcesTable } from "../table.js";
|
|
4
5
|
import { executor, upsertPayloadSchema } from "./shared.js";
|
|
5
6
|
// System-Template anlegen/updaten. Liegt unter SYSTEM_TENANT_ID,
|
|
@@ -12,13 +13,10 @@ export const upsertSystemWrite = defineWriteHandler({
|
|
|
12
13
|
access: { roles: ["SystemAdmin"] },
|
|
13
14
|
description: "Creates or overwrites a system-default template under the system tenant with its full variable schema and linked resources, active straight away with no draft stage; SystemAdmin only, since tenants shadow these defaults through upsert-tenant instead.",
|
|
14
15
|
handler: async (event, ctx) => {
|
|
15
|
-
const db = ctx.db;
|
|
16
16
|
// @cast-boundary engine-payload — SYSTEM_TENANT_ID is a UUID literal,
|
|
17
17
|
// asserted to the branded TenantId (parseTenantId equivalent).
|
|
18
18
|
const tenantId = SYSTEM_TENANT_ID;
|
|
19
|
-
|
|
20
|
-
// event store looks up the stream under user.tenantId → conflict.
|
|
21
|
-
const executorUser = { ...event.user, tenantId };
|
|
19
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
22
20
|
const existing = await fetchOne(db, templateResourcesTable, {
|
|
23
21
|
tenantId,
|
|
24
22
|
slug: event.payload.slug,
|
|
@@ -41,7 +39,7 @@ export const upsertSystemWrite = defineWriteHandler({
|
|
|
41
39
|
status: "active",
|
|
42
40
|
};
|
|
43
41
|
if (existing) {
|
|
44
|
-
const result = await executor.update({ id: existing.id, version: existing.version, changes: fields },
|
|
42
|
+
const result = await executor.update({ id: existing.id, version: existing.version, changes: fields }, event.user, db, { streamTenantId });
|
|
45
43
|
if (!result.isSuccess)
|
|
46
44
|
return result;
|
|
47
45
|
return {
|
|
@@ -49,7 +47,9 @@ export const upsertSystemWrite = defineWriteHandler({
|
|
|
49
47
|
data: { id: String(existing.id), slug: event.payload.slug, isNew: false },
|
|
50
48
|
};
|
|
51
49
|
}
|
|
52
|
-
const result = await executor.create({ ...fields, tenantId },
|
|
50
|
+
const result = await executor.create({ ...fields, tenantId }, event.user, db, {
|
|
51
|
+
streamTenantId,
|
|
52
|
+
});
|
|
53
53
|
if (!result.isSuccess)
|
|
54
54
|
return result;
|
|
55
55
|
// @cast-boundary db-row — executor.create returnt Record-row aus
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import {
|
|
2
|
+
import { defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { AccessDeniedError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import * as z from "zod";
|
|
5
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
5
6
|
import { templateResourcesTable } from "../table.js";
|
|
6
7
|
import { executor, upsertPayloadSchema } from "./shared.js";
|
|
7
8
|
// Tenant-Override anlegen/updaten. Liegt unter event.user.tenantId,
|
|
@@ -18,11 +19,7 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
18
19
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
19
20
|
description: "Creates or overwrites a tenant-scoped template override with its variable schema, landing as a draft that only publish makes live; it refuses a system-tenant target, which is what upsert-system is for.",
|
|
20
21
|
handler: async (event, ctx) => {
|
|
21
|
-
const db = ctx.db;
|
|
22
22
|
const override = event.payload.tenantIdOverride;
|
|
23
|
-
const overrideDenied = crossTenantOverrideDenied(event.user, override, "templateResolver.errors.tenantOverrideRequiresSystemAdmin");
|
|
24
|
-
if (overrideDenied)
|
|
25
|
-
return writeFailure(overrideDenied);
|
|
26
23
|
// upsertTenant erzeugt scope='tenant'. SYSTEM_TENANT_ID-Override würde
|
|
27
24
|
// scope='tenant' unter SYSTEM_TENANT_ID schreiben → inkonsistenter Zustand
|
|
28
25
|
// (Resolver-Logik trennt sauber zwischen system+tenant). SystemAdmin muss
|
|
@@ -37,7 +34,7 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
37
34
|
// event.user.tenantId schon TenantId-branded; union als TenantId casten
|
|
38
35
|
// ist legit (override ist UUID-Format-validiert in schema).
|
|
39
36
|
const tenantId = (override ?? event.user.tenantId);
|
|
40
|
-
const
|
|
37
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
41
38
|
const existing = await fetchOne(db, templateResourcesTable, {
|
|
42
39
|
tenantId,
|
|
43
40
|
slug: event.payload.slug,
|
|
@@ -59,7 +56,7 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
59
56
|
status: event.payload.status,
|
|
60
57
|
};
|
|
61
58
|
if (existing) {
|
|
62
|
-
const result = await executor.update({ id: existing.id, version: existing.version, changes: fields },
|
|
59
|
+
const result = await executor.update({ id: existing.id, version: existing.version, changes: fields }, event.user, db, { streamTenantId });
|
|
63
60
|
if (!result.isSuccess)
|
|
64
61
|
return result;
|
|
65
62
|
return {
|
|
@@ -67,7 +64,9 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
67
64
|
data: { id: String(existing.id), slug: event.payload.slug, isNew: false },
|
|
68
65
|
};
|
|
69
66
|
}
|
|
70
|
-
const result = await executor.create({ ...fields, tenantId },
|
|
67
|
+
const result = await executor.create({ ...fields, tenantId }, event.user, db, {
|
|
68
|
+
streamTenantId,
|
|
69
|
+
});
|
|
71
70
|
if (!result.isSuccess)
|
|
72
71
|
return result;
|
|
73
72
|
// @cast-boundary db-row — executor.create returnt Record-row aus
|
|
@@ -29,8 +29,7 @@ async function resolveExistingForEventStoreSeed(db, tdb, existingRow, tenantId)
|
|
|
29
29
|
if (unscoped !== 0) {
|
|
30
30
|
throw new Error(`seed orphan check: projection ${id} has no events for tenant ${tenantId} but unscoped stream version is ${unscoped} — refusing delete/recreate to avoid wiping a live stream`);
|
|
31
31
|
}
|
|
32
|
-
//
|
|
33
|
-
// plain table ident trips guard-direct-entity-writes (infra EXCLUDE follow-up).
|
|
32
|
+
// The cast strips the executor-only brand that WritableTable rejects; the orphan row has no events, so no executor verb applies.
|
|
34
33
|
await deleteMany(tdb, templateResourcesTable, { id, tenantId });
|
|
35
34
|
return null;
|
|
36
35
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import type { WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import type { Redis } from "ioredis";
|
|
5
5
|
export declare function cancelPendingInvitation(db: TenantDb, invitation: {
|
|
6
6
|
readonly id: string;
|
|
7
7
|
readonly version: number;
|
|
8
|
-
}, actor: SessionUser, redis: Redis | undefined): Promise<WriteFailure | undefined>;
|
|
8
|
+
}, actor: SessionUser, redis: Redis | undefined, streamTenantId?: TenantId): Promise<WriteFailure | undefined>;
|
|
@@ -5,12 +5,12 @@ import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } fr
|
|
|
5
5
|
const invitationExecutor = createEventStoreExecutor(tenantInvitationsTable, tenantInvitationEntity, { entityName: "tenant-invitation" });
|
|
6
6
|
// Shared by cancel-invitation and remove-member so the cancel behavior
|
|
7
7
|
// (status transition + token invalidation) cannot drift between them.
|
|
8
|
-
export async function cancelPendingInvitation(db, invitation, actor, redis) {
|
|
8
|
+
export async function cancelPendingInvitation(db, invitation, actor, redis, streamTenantId) {
|
|
9
9
|
const updateResult = await invitationExecutor.update({
|
|
10
10
|
id: invitation.id,
|
|
11
11
|
version: invitation.version,
|
|
12
12
|
changes: { status: INVITATION_STATUS.cancelled },
|
|
13
|
-
}, actor, db);
|
|
13
|
+
}, actor, db, { streamTenantId });
|
|
14
14
|
if (!updateResult.isSuccess)
|
|
15
15
|
return updateResult;
|
|
16
16
|
// Delete the token from Redis (if still there). If Redis is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { createEventStoreExecutor, } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import { createSystemUser, defineWriteHandler, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { createSystemUser, defineWriteHandler, parseTenantId, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { InternalError, NotFoundError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
5
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
6
6
|
import * as z from "zod";
|
|
@@ -20,7 +20,7 @@ async function cancelPendingInvitationsOfUser(db, options) {
|
|
|
20
20
|
const email = (await decryptStoredPii(user.email, "email", "tenant:remove-member")).toLowerCase();
|
|
21
21
|
const pendingInvitations = await selectMany(db, tenantInvitationsTable, { tenantId: options.tenantId, email, status: INVITATION_STATUS.pending });
|
|
22
22
|
for (const invitation of pendingInvitations) {
|
|
23
|
-
const failure = await cancelPendingInvitation(db, invitation, options.actor, options.redis);
|
|
23
|
+
const failure = await cancelPendingInvitation(db, invitation, options.actor, options.redis, options.streamTenantId);
|
|
24
24
|
if (failure)
|
|
25
25
|
return failure;
|
|
26
26
|
}
|
|
@@ -43,11 +43,14 @@ export const removeMemberWrite = defineWriteHandler({
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
const db = ctx.systemDb.acknowledgeCrossTenant("SystemAdmin manages memberships across tenants");
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
const streamTenantId = parseTenantId(event.payload.tenantId);
|
|
47
|
+
const existing = streamTenantId === null
|
|
48
|
+
? undefined
|
|
49
|
+
: await fetchOne(db, tenantMembershipsTable, {
|
|
50
|
+
userId: event.payload.userId,
|
|
51
|
+
tenantId: streamTenantId,
|
|
52
|
+
});
|
|
53
|
+
if (!existing || streamTenantId === null) {
|
|
51
54
|
return writeFailure(new NotFoundError("membership", undefined, {
|
|
52
55
|
i18nKey: "tenant.errors.membershipNotFound",
|
|
53
56
|
i18nParams: { userId: event.payload.userId, tenantId: event.payload.tenantId },
|
|
@@ -75,14 +78,14 @@ export const removeMemberWrite = defineWriteHandler({
|
|
|
75
78
|
});
|
|
76
79
|
}
|
|
77
80
|
const result = await executor.delete({ id: existing["id"] }, // @cast-boundary db-row
|
|
78
|
-
event.user, db);
|
|
81
|
+
event.user, db, { streamTenantId });
|
|
79
82
|
if (!result.isSuccess)
|
|
80
83
|
return result;
|
|
81
|
-
// Actor tenant = invitation tenant so the update hits the invitation's stream.
|
|
82
84
|
const cancelFailure = await cancelPendingInvitationsOfUser(db, {
|
|
83
85
|
userId: event.payload.userId,
|
|
84
86
|
tenantId: event.payload.tenantId,
|
|
85
|
-
actor:
|
|
87
|
+
actor: event.user,
|
|
88
|
+
streamTenantId,
|
|
86
89
|
redis: ctx.redis,
|
|
87
90
|
});
|
|
88
91
|
if (cancelFailure !== undefined)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
-
import { access, assignableAppRolesOf, createSystemUser, defineWriteHandler, findForbiddenRoleAssignment, mergeAssignedRoles, SYSTEM_ROLE, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { access, assignableAppRolesOf, createSystemUser, defineWriteHandler, findForbiddenRoleAssignment, mayOverrideTenant, mergeAssignedRoles, parseTenantId, SYSTEM_ROLE, withResponseData, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { InternalError, NotFoundError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
5
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
6
6
|
import * as z from "zod";
|
|
@@ -17,7 +17,12 @@ const REVOKE_ALL_SESSIONS_QN = "sessions:write:user-session:revoke-all-for-user"
|
|
|
17
17
|
/** SystemAdmin may pass payload.tenantId (cross-tenant); the members actionForm
|
|
18
18
|
* only sends userId+roles, so fall back to the active session tenant. */
|
|
19
19
|
function resolveTargetTenantId(isSystem, payloadTenantId, sessionTenantId) {
|
|
20
|
-
|
|
20
|
+
if (!isSystem || payloadTenantId === undefined)
|
|
21
|
+
return sessionTenantId;
|
|
22
|
+
return parseTenantId(payloadTenantId) ?? undefined;
|
|
23
|
+
}
|
|
24
|
+
function foreignStreamTenant(user, targetTenantId) {
|
|
25
|
+
return targetTenantId === user.tenantId ? undefined : targetTenantId;
|
|
21
26
|
}
|
|
22
27
|
// Editing your OWN roles keeps the current session: roles are re-derived per
|
|
23
28
|
// request, so the new roles apply at once without a re-login. Other sessions
|
|
@@ -49,7 +54,7 @@ export const updateMemberRolesWrite = defineWriteHandler({
|
|
|
49
54
|
});
|
|
50
55
|
}
|
|
51
56
|
const isSystemActor = event.user.roles.includes(SYSTEM_ROLE);
|
|
52
|
-
const isSystem = event.user
|
|
57
|
+
const isSystem = mayOverrideTenant(event.user) || isSystemActor;
|
|
53
58
|
const assignableAppRoles = assignableAppRolesOf(ctx.registry);
|
|
54
59
|
const targetTenantId = resolveTargetTenantId(isSystem, event.payload.tenantId, event.user.tenantId);
|
|
55
60
|
if (!targetTenantId) {
|
|
@@ -117,16 +122,14 @@ export const updateMemberRolesWrite = defineWriteHandler({
|
|
|
117
122
|
if (revoker) {
|
|
118
123
|
await ctx.writeAs(createSystemUser(targetTenantId), REVOKE_ALL_SESSIONS_QN, revokeSessionsPayload(event.payload.userId, targetTenantId, event.user));
|
|
119
124
|
}
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
// Keep the real actor for audit metadata; only override tenantId so
|
|
124
|
-
// streamTenantFor writes into the membership tenant (#2401).
|
|
125
|
+
// The membership stream lives on the membership's tenant (where seedTenantMembership/
|
|
126
|
+
// invite-accept wrote it); a cross-tenant operator addresses it without becoming a member.
|
|
127
|
+
// Same-tenant writes run on a tenant-mode db, which refuses a stream override.
|
|
125
128
|
const result = await executor.update({
|
|
126
129
|
id: row["id"], // @cast-boundary db-row
|
|
127
130
|
version: row["version"], // @cast-boundary db-row
|
|
128
131
|
changes: { roles: JSON.stringify(nextRoles) },
|
|
129
|
-
}, {
|
|
132
|
+
}, event.user, db, { streamTenantId: foreignStreamTenant(event.user, targetTenantId) });
|
|
130
133
|
return withResponseData(result, {
|
|
131
134
|
...event.payload,
|
|
132
135
|
roles: nextRoles,
|
|
@@ -89,6 +89,7 @@ export function createTenantLifecycleFeature() {
|
|
|
89
89
|
fileProviderResolver: ctx._fileProviderResolver,
|
|
90
90
|
searchAdapter: ctx.searchAdapter,
|
|
91
91
|
escapeHatchAuditSink: ctx._escapeHatchAuditSink,
|
|
92
|
+
escapeHatchAuditLog: ctx.log,
|
|
92
93
|
actor: ctx.systemUser.id,
|
|
93
94
|
});
|
|
94
95
|
},
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import { type EscapeHatchAuditSink, type Registry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
|
|
5
|
+
import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
5
6
|
import type { SearchAdapter } from "@cosmicdrift/kumiko-framework/search";
|
|
6
7
|
type StageTickResult = {
|
|
7
8
|
readonly done: boolean;
|
|
@@ -17,6 +18,7 @@ export declare function runNextDestructionStage(args: {
|
|
|
17
18
|
readonly fileProviderResolver?: FileProviderResolver;
|
|
18
19
|
readonly searchAdapter?: SearchAdapter;
|
|
19
20
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
21
|
+
readonly escapeHatchAuditLog?: Logger;
|
|
20
22
|
readonly actor?: string;
|
|
21
23
|
}): Promise<StageTickResult>;
|
|
22
24
|
export declare function hasTenantDestructionWork(reads: Pick<TenantDb, "count">, now: Temporal.Instant): Promise<boolean>;
|
|
@@ -29,6 +31,7 @@ export declare function runTenantDestructionSweep(args: {
|
|
|
29
31
|
readonly fileProviderResolver?: FileProviderResolver;
|
|
30
32
|
readonly searchAdapter?: SearchAdapter;
|
|
31
33
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
34
|
+
readonly escapeHatchAuditLog?: Logger;
|
|
32
35
|
readonly actor?: string;
|
|
33
36
|
}): Promise<{
|
|
34
37
|
readonly triggered: number;
|
|
@@ -162,6 +162,7 @@ export async function runNextDestructionStage(args) {
|
|
|
162
162
|
fileProviderResolver: args.fileProviderResolver,
|
|
163
163
|
searchAdapter: args.searchAdapter,
|
|
164
164
|
escapeHatchAuditSink: args.escapeHatchAuditSink,
|
|
165
|
+
escapeHatchAuditLog: args.escapeHatchAuditLog,
|
|
165
166
|
actor: args.actor,
|
|
166
167
|
deadlineAt: Date.now() + DESTRUCTION_STAGE_TICK_BUDGET_MS,
|
|
167
168
|
};
|
|
@@ -258,6 +259,7 @@ export async function runTenantDestructionSweep(args) {
|
|
|
258
259
|
fileProviderResolver: args.fileProviderResolver,
|
|
259
260
|
searchAdapter: args.searchAdapter,
|
|
260
261
|
escapeHatchAuditSink: args.escapeHatchAuditSink,
|
|
262
|
+
escapeHatchAuditLog: args.escapeHatchAuditLog,
|
|
261
263
|
actor: args.actor,
|
|
262
264
|
});
|
|
263
265
|
if (!result.error && !result.halted)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { type EscapeHatchAuditSink, type Registry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import type { FileProviderResolver } from "@cosmicdrift/kumiko-framework/files";
|
|
4
|
+
import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
4
5
|
import { type SearchAdapter } from "@cosmicdrift/kumiko-framework/search";
|
|
5
6
|
import type { TenantDestructionStageName } from "./constants.js";
|
|
6
7
|
export type DestructionStageCtx = {
|
|
@@ -11,6 +12,7 @@ export type DestructionStageCtx = {
|
|
|
11
12
|
readonly fileProviderResolver?: FileProviderResolver;
|
|
12
13
|
readonly searchAdapter?: SearchAdapter;
|
|
13
14
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
15
|
+
readonly escapeHatchAuditLog?: Logger;
|
|
14
16
|
readonly actor?: string;
|
|
15
17
|
readonly deadlineAt: number;
|
|
16
18
|
};
|
|
@@ -2,6 +2,7 @@ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
|
2
2
|
import { configuredPiiSubjectKms, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
3
3
|
import { createEventStoreExecutor, createTenantDb, } from "@cosmicdrift/kumiko-framework/db";
|
|
4
4
|
import { createSystemUser, EXT_EXTERNAL_RESOURCE, EXT_INFRA_RESOURCE, EXT_SEARCH_ADAPTER, EXT_STORAGE_PROVIDER, EXT_TENANT_DATA, extensionUsageEscapeHatchReason, isTenantDataExtensionHooks, isTenantResourceExtensionHooks, } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
|
+
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
5
6
|
import { createEscapeHatchReporter, UNATTRIBUTED_ACTOR, } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
6
7
|
import { purgeSearchDocumentsForSubject, } from "@cosmicdrift/kumiko-framework/search";
|
|
7
8
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
@@ -51,6 +52,7 @@ async function runTenantDataHooks(ctx) {
|
|
|
51
52
|
tenantId: ctx.tenantId,
|
|
52
53
|
actor: ctx.actor ?? UNATTRIBUTED_ACTOR,
|
|
53
54
|
sink: ctx.escapeHatchAuditSink,
|
|
55
|
+
log: ctx.escapeHatchAuditLog ?? createFallbackLogger("tenant-lifecycle"),
|
|
54
56
|
});
|
|
55
57
|
const hookCtx = {
|
|
56
58
|
db: createTenantDb(ctx.db, ctx.tenantId, "tenant", undefined, undefined, undefined, {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import { buildEntityTable, createEventStoreExecutor
|
|
2
|
+
import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import * as z from "zod";
|
|
5
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
5
6
|
import { tierAssignmentAggregateId } from "../aggregate-id.js";
|
|
6
7
|
import { TierAssignmentSources } from "../constants.js";
|
|
7
8
|
import { tierAssignmentEntity } from "../entity.js";
|
|
@@ -9,13 +10,8 @@ import { tierAssignmentEntity } from "../entity.js";
|
|
|
9
10
|
// Billing. Cross-tenant, daher SystemAdmin-only (kein TenantAdmin: sonst
|
|
10
11
|
// Gratis-Self-Upgrade).
|
|
11
12
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// der executor-user wird ebenfalls auf den Ziel-Tenant gestellt, sonst landet das
|
|
15
|
-
// Event im Stream des Admins (Memory feedback_event_store_tenant_consistency).
|
|
16
|
-
// Das set.write-"override-user"-Muster trägt NICHT für beliebige Tenants — es
|
|
17
|
-
// funktioniert nur für SYSTEM_TENANT_ID (immer im IN-Filter). Dies ist das
|
|
18
|
-
// auto-default-Hook-Muster (feature.ts), generalisiert auf einen Request-Handler.
|
|
13
|
+
// Cross-tenant mechanics: ctx.forTenant(tenantId) yields a tenant-mode db bound to the target plus
|
|
14
|
+
// the streamTenantId, so the event lands in the target's stream with the operator as actor.
|
|
19
15
|
//
|
|
20
16
|
// `source: TierAssignmentSources.manual` marks the grant so the billing sync skips it.
|
|
21
17
|
// Upsert: one aggregate per tenant (deterministic aggregate id).
|
|
@@ -31,7 +27,6 @@ const tierAssignmentTable = buildEntityTable("tier-assignment", tierAssignmentEn
|
|
|
31
27
|
const executor = createEventStoreExecutor(tierAssignmentTable, tierAssignmentEntity, {
|
|
32
28
|
entityName: "tier-assignment",
|
|
33
29
|
});
|
|
34
|
-
const SET_TENANT_TIER_REASON = "SystemAdmin assigns the tier of the tenant named in the payload";
|
|
35
30
|
export function createSetTenantTierWrite(opts = {}) {
|
|
36
31
|
return defineWriteHandler({
|
|
37
32
|
name: "set-tenant-tier",
|
|
@@ -46,10 +41,6 @@ export function createSetTenantTierWrite(opts = {}) {
|
|
|
46
41
|
}),
|
|
47
42
|
access: { roles: ["SystemAdmin"] },
|
|
48
43
|
agent: { risk: "high" },
|
|
49
|
-
escapeHatch: {
|
|
50
|
-
grants: ["unsafeRaw"],
|
|
51
|
-
reason: SET_TENANT_TIER_REASON,
|
|
52
|
-
},
|
|
53
44
|
handler: async (event, ctx) => {
|
|
54
45
|
// Without a commit sink (unit harness) there is no transaction to wait for.
|
|
55
46
|
const afterCommit = (tenantId, tier) => {
|
|
@@ -60,9 +51,7 @@ export function createSetTenantTierWrite(opts = {}) {
|
|
|
60
51
|
opts.onAssigned?.(tenantId, tier);
|
|
61
52
|
};
|
|
62
53
|
const tenantId = event.payload.tenantId; // @cast-boundary engine-bridge
|
|
63
|
-
const
|
|
64
|
-
const tdb = createTenantDb(rawDb, tenantId, "system");
|
|
65
|
-
const systemUser = { ...event.user, tenantId };
|
|
54
|
+
const { db: tdb, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
66
55
|
const tier = event.payload.tier;
|
|
67
56
|
const existing = await fetchOne(tdb, tierAssignmentTable, { tenantId });
|
|
68
57
|
if (existing) {
|
|
@@ -70,7 +59,7 @@ export function createSetTenantTierWrite(opts = {}) {
|
|
|
70
59
|
id: existing.id,
|
|
71
60
|
version: existing.version,
|
|
72
61
|
changes: { tier, source: TierAssignmentSources.manual },
|
|
73
|
-
},
|
|
62
|
+
}, event.user, tdb, { streamTenantId });
|
|
74
63
|
if (!result.isSuccess)
|
|
75
64
|
return result;
|
|
76
65
|
afterCommit(tenantId, tier);
|
|
@@ -81,7 +70,7 @@ export function createSetTenantTierWrite(opts = {}) {
|
|
|
81
70
|
tier,
|
|
82
71
|
source: TierAssignmentSources.manual,
|
|
83
72
|
tenantId,
|
|
84
|
-
},
|
|
73
|
+
}, event.user, tdb, { streamTenantId });
|
|
85
74
|
if (!result.isSuccess)
|
|
86
75
|
return result;
|
|
87
76
|
afterCommit(tenantId, tier);
|
|
@@ -477,6 +477,7 @@ export function createUserDataRightsFeature(opts = {}) {
|
|
|
477
477
|
appExportDownloadUrl: opts.appExportDownloadUrl,
|
|
478
478
|
}),
|
|
479
479
|
escapeHatchAuditSink: ctx._escapeHatchAuditSink,
|
|
480
|
+
escapeHatchAuditLog: ctx.log,
|
|
480
481
|
actor: ctx.systemUser.id,
|
|
481
482
|
metrics: exportMetrics,
|
|
482
483
|
});
|
|
@@ -541,6 +542,7 @@ export function createUserDataRightsFeature(opts = {}) {
|
|
|
541
542
|
}),
|
|
542
543
|
...(sendDeletionExecutedEmail && { sendDeletionExecutedEmail }),
|
|
543
544
|
escapeHatchAuditSink: ctx._escapeHatchAuditSink,
|
|
545
|
+
escapeHatchAuditLog: ctx.log,
|
|
544
546
|
actor: ctx.systemUser.id,
|
|
545
547
|
});
|
|
546
548
|
// Persist each incomplete report on the job-run log (#1572). The write
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
1
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
import type { EscapeHatchAuditSink, Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
4
|
import { type FileStorageProvider } from "@cosmicdrift/kumiko-framework/files";
|
|
@@ -75,6 +76,7 @@ export interface RunExportJobsArgs {
|
|
|
75
76
|
* Callback durch. Required wenn sendExportReadyEmail gesetzt. */
|
|
76
77
|
readonly appExportDownloadUrl?: string;
|
|
77
78
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
79
|
+
readonly escapeHatchAuditLog?: Logger;
|
|
78
80
|
readonly actor?: string;
|
|
79
81
|
/**
|
|
80
82
|
* Reports EXPORT_CLEANUP_BACKLOG_AGE_METRIC after the storage-cleanup
|
|
@@ -106,6 +106,7 @@ export async function runExportJobs(args) {
|
|
|
106
106
|
now,
|
|
107
107
|
job,
|
|
108
108
|
escapeHatchAuditSink: args.escapeHatchAuditSink,
|
|
109
|
+
escapeHatchAuditLog: args.escapeHatchAuditLog,
|
|
109
110
|
actor: args.actor,
|
|
110
111
|
});
|
|
111
112
|
if (outcome.kind === "done") {
|
|
@@ -230,6 +231,7 @@ async function processJob(args) {
|
|
|
230
231
|
userId: job.userId,
|
|
231
232
|
now,
|
|
232
233
|
escapeHatchAuditSink: args.escapeHatchAuditSink,
|
|
234
|
+
escapeHatchAuditLog: args.escapeHatchAuditLog,
|
|
233
235
|
actor: args.actor,
|
|
234
236
|
});
|
|
235
237
|
const profile = await resolveProfileForTenant({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
1
2
|
import { type KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
2
3
|
import { type DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
3
4
|
import { type EscapeHatchAuditSink, type Registry, type TenantId, type TenantUserModel, type UserDataDeleteStrategy, type UserDataStorageProvider } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -69,6 +70,7 @@ export interface RunForgetCleanupArgs {
|
|
|
69
70
|
*/
|
|
70
71
|
readonly tenantModel?: TenantUserModel;
|
|
71
72
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
73
|
+
readonly escapeHatchAuditLog?: Logger;
|
|
72
74
|
readonly actor?: string;
|
|
73
75
|
}
|
|
74
76
|
export interface ForgetCleanupError {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
1
2
|
// Forget-Cleanup-Runner (S2.U5b) — pure-Function Pipeline.
|
|
2
3
|
//
|
|
3
4
|
// After the grace period (S2.U5a sets gracePeriodEnd) this runner iterates
|
|
@@ -51,6 +52,7 @@ function buildHookDb(db, tenantId, entry, args) {
|
|
|
51
52
|
tenantId,
|
|
52
53
|
actor: args.actor ?? UNATTRIBUTED_ACTOR,
|
|
53
54
|
sink: args.escapeHatchAuditSink,
|
|
55
|
+
log: args.escapeHatchAuditLog ?? createFallbackLogger("user-data-rights"),
|
|
54
56
|
});
|
|
55
57
|
return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
|
|
56
58
|
unsafeRaw: entry.escapeHatchReason !== undefined ? { reason: entry.escapeHatchReason } : undefined,
|
|
@@ -117,6 +119,7 @@ export async function runForgetCleanup(args) {
|
|
|
117
119
|
kms,
|
|
118
120
|
searchAdapter: args.searchAdapter,
|
|
119
121
|
escapeHatchAuditSink: args.escapeHatchAuditSink,
|
|
122
|
+
escapeHatchAuditLog: args.escapeHatchAuditLog,
|
|
120
123
|
actor: args.actor,
|
|
121
124
|
});
|
|
122
125
|
hookCallsAttempted += userResult.hookCallsAttempted;
|
|
@@ -235,6 +238,7 @@ async function processUser(args) {
|
|
|
235
238
|
const hookResult = await entry.deleteHook({
|
|
236
239
|
db: buildHookDb(tx, tenantId, entry, {
|
|
237
240
|
escapeHatchAuditSink: args.escapeHatchAuditSink,
|
|
241
|
+
escapeHatchAuditLog: args.escapeHatchAuditLog,
|
|
238
242
|
actor: args.actor,
|
|
239
243
|
}),
|
|
240
244
|
registry,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
|
|
1
2
|
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
import { type EscapeHatchAuditSink, type Registry, type TenantId, type UserDataExportSnippet } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
4
|
import type { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
@@ -8,6 +9,7 @@ export interface RunUserExportArgs {
|
|
|
8
9
|
readonly userId: string;
|
|
9
10
|
readonly now: Instant;
|
|
10
11
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
12
|
+
readonly escapeHatchAuditLog?: Logger;
|
|
11
13
|
readonly actor?: string;
|
|
12
14
|
}
|
|
13
15
|
export interface UserExportFileRef {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
1
2
|
// User-Data-Export-Pipeline (S2.U3) — DSGVO Art. 15 (Auskunft) +
|
|
2
3
|
// Art. 20 (Datenportabilität).
|
|
3
4
|
//
|
|
@@ -32,6 +33,7 @@ function buildHookDb(db, tenantId, entry, args) {
|
|
|
32
33
|
tenantId,
|
|
33
34
|
actor: args.actor ?? UNATTRIBUTED_ACTOR,
|
|
34
35
|
sink: args.escapeHatchAuditSink,
|
|
36
|
+
log: args.escapeHatchAuditLog ?? createFallbackLogger("user-data-rights"),
|
|
35
37
|
});
|
|
36
38
|
return createTenantDb(db, tenantId, "tenant", undefined, undefined, undefined, {
|
|
37
39
|
unsafeRaw: entry.escapeHatchReason !== undefined ? { reason: entry.escapeHatchReason } : undefined,
|
package/package.json
CHANGED
|
@@ -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>",
|
|
@@ -516,12 +516,12 @@
|
|
|
516
516
|
}
|
|
517
517
|
},
|
|
518
518
|
"dependencies": {
|
|
519
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
520
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
521
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
522
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
523
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
524
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
519
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.356.0",
|
|
520
|
+
"@cosmicdrift/kumiko-framework": "0.356.0",
|
|
521
|
+
"@cosmicdrift/kumiko-headless": "0.356.0",
|
|
522
|
+
"@cosmicdrift/kumiko-renderer": "0.356.0",
|
|
523
|
+
"@cosmicdrift/kumiko-renderer-web": "0.356.0",
|
|
524
|
+
"@cosmicdrift/kumiko-types": "0.356.0",
|
|
525
525
|
"@mollie/api-client": "^4.5.0",
|
|
526
526
|
"@node-rs/argon2": "^2.0.2",
|
|
527
527
|
"@types/mailparser": "^3.4.6",
|
|
@@ -1050,8 +1050,8 @@
|
|
|
1050
1050
|
],
|
|
1051
1051
|
"devDependencies": {
|
|
1052
1052
|
"@testing-library/user-event": "^14.6.1",
|
|
1053
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
1054
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
1053
|
+
"@cosmicdrift/kumiko-locale-de": "0.356.0",
|
|
1054
|
+
"@cosmicdrift/kumiko-locale-es": "0.356.0",
|
|
1055
1055
|
"jsqr": "^1.4.0"
|
|
1056
1056
|
}
|
|
1057
1057
|
}
|