@cosmicdrift/kumiko-bundled-features 0.355.0 → 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/compliance-profiles/handlers/set-profile.write.js +14 -22
- 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/managed-pages/handlers/set.write.js +6 -17
- 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/collection-set.write.js +4 -4
- package/dist/template-resolver/handlers/set.write.js +6 -7
- package/dist/template-resolver/handlers/upsert-system.write.js +6 -6
- package/dist/template-resolver/handlers/upsert-tenant.write.js +6 -4
- 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/tier-engine/handlers/set-tenant-tier.write.js +7 -18
- package/package.json +9 -9
|
@@ -4,6 +4,7 @@ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
|
4
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,9 +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
|
-
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);
|
|
49
41
|
// Override-Validation: muss parseables JSON-Object sein UND dem
|
|
50
42
|
// ComplianceProfileOverride-Schema entsprechen (S1.9 Z3 — strict-Zod
|
|
51
43
|
// mit Top-Level + Sub-Level-Whitelist via .strict()). Tippfehler
|
|
@@ -72,7 +64,7 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
72
64
|
}
|
|
73
65
|
}
|
|
74
66
|
// Upsert: existierenden Eintrag suchen
|
|
75
|
-
const existing = (await fetchOne(
|
|
67
|
+
const existing = (await fetchOne(db, tenantComplianceProfileTable, {
|
|
76
68
|
tenantId: tenantId,
|
|
77
69
|
})); // @cast-boundary db-runner
|
|
78
70
|
if (existing) {
|
|
@@ -83,7 +75,7 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
83
75
|
profileKey: event.payload.profileKey,
|
|
84
76
|
override: event.payload.override ?? null,
|
|
85
77
|
},
|
|
86
|
-
},
|
|
78
|
+
}, event.user, db, { streamTenantId });
|
|
87
79
|
if (!result.isSuccess)
|
|
88
80
|
return result;
|
|
89
81
|
return {
|
|
@@ -95,7 +87,7 @@ export const setProfileWrite = defineWriteHandler({
|
|
|
95
87
|
profileKey: event.payload.profileKey,
|
|
96
88
|
override: event.payload.override ?? null,
|
|
97
89
|
tenantId,
|
|
98
|
-
},
|
|
90
|
+
}, event.user, db, { streamTenantId });
|
|
99
91
|
if (!result.isSuccess)
|
|
100
92
|
return result;
|
|
101
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
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import { createEventStoreExecutor
|
|
2
|
+
import { 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 { pageEntity, pagesTable } from "../table.js";
|
|
6
7
|
const slugSchema = z
|
|
7
8
|
.string()
|
|
@@ -14,7 +15,6 @@ const langSchema = z
|
|
|
14
15
|
.max(8)
|
|
15
16
|
.regex(/^[a-z]{2}(-[a-z]{2})?$/i, "lang must be ISO 639-1 (e.g. de, en, en-us)");
|
|
16
17
|
const executor = createEventStoreExecutor(pagesTable, pageEntity, { entityName: "page" });
|
|
17
|
-
const SET_PAGE_TENANT_OVERRIDE_REASON = "SystemAdmin tenantIdOverride re-scopes the page write to the target tenant";
|
|
18
18
|
// Upsert einer Page — eine Operation pro (tenantId, slug, lang). Tenant-
|
|
19
19
|
// Scope default aus event.user; SystemAdmin kann via `tenantIdOverride`
|
|
20
20
|
// für einen anderen Tenant schreiben (typisch SYSTEM_TENANT_ID für app-
|
|
@@ -36,23 +36,12 @@ export const setWrite = defineWriteHandler({
|
|
|
36
36
|
}),
|
|
37
37
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
38
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.",
|
|
39
|
-
escapeHatch: {
|
|
40
|
-
grants: ["unsafeRaw"],
|
|
41
|
-
reason: SET_PAGE_TENANT_OVERRIDE_REASON,
|
|
42
|
-
},
|
|
43
39
|
handler: async (event, ctx) => {
|
|
44
|
-
const db = ctx.db;
|
|
45
40
|
const override = event.payload.tenantIdOverride;
|
|
46
|
-
const tenantId = override ?? event.user.tenantId;
|
|
47
|
-
// override: point the executor context at the target tenant, else getStreamVersion runs against user.tenantId → version_conflict.
|
|
48
|
-
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
|
|
49
42
|
// ctx.db is scoped to the executing user's tenant, wrong for a cross-tenant override
|
|
50
43
|
// on both the existing-check and the executor's stream reads; re-scope to the target.
|
|
51
|
-
|
|
52
|
-
const scopedDb = override !== undefined
|
|
53
|
-
? createTenantDb(db.unsafeRaw(), override, // @cast-boundary engine-bridge
|
|
54
|
-
"tenant")
|
|
55
|
-
: db;
|
|
44
|
+
const { db: scopedDb, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
56
45
|
const existing = await fetchOne(scopedDb, pagesTable, {
|
|
57
46
|
tenantId,
|
|
58
47
|
slug: event.payload.slug,
|
|
@@ -71,7 +60,7 @@ export const setWrite = defineWriteHandler({
|
|
|
71
60
|
ogImage: event.payload.ogImage !== undefined ? event.payload.ogImage : existing.ogImage,
|
|
72
61
|
published: event.payload.published !== undefined ? event.payload.published : existing.published,
|
|
73
62
|
},
|
|
74
|
-
},
|
|
63
|
+
}, event.user, scopedDb, { streamTenantId });
|
|
75
64
|
if (!result.isSuccess)
|
|
76
65
|
return result;
|
|
77
66
|
return {
|
|
@@ -88,7 +77,7 @@ export const setWrite = defineWriteHandler({
|
|
|
88
77
|
ogImage: event.payload.ogImage ?? null,
|
|
89
78
|
published: event.payload.published ?? false,
|
|
90
79
|
tenantId,
|
|
91
|
-
},
|
|
80
|
+
}, event.user, scopedDb, { streamTenantId });
|
|
92
81
|
if (!result.isSuccess)
|
|
93
82
|
return result;
|
|
94
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,6 +1,7 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
4
5
|
import { collectionStore, DEFAULT_COLLECTION_ACCESS, } from "./collection-shared.js";
|
|
5
6
|
import { contentFormatSchema, folderSchema, localeSchema, slugSchema } from "./shared.js";
|
|
6
7
|
// Upsert inside one collection. Mirrors set.write for text-blocks, but the
|
|
@@ -29,12 +30,11 @@ export function makeCollectionSetWrite(collection) {
|
|
|
29
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.`
|
|
30
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.`,
|
|
31
32
|
handler: async (event, ctx) => {
|
|
32
|
-
const db = ctx.db;
|
|
33
33
|
const override = event.payload.tenantIdOverride;
|
|
34
34
|
// @cast-boundary engine-payload — override is a zod-validated string, the
|
|
35
35
|
// user's own tenantId is already TenantId-branded.
|
|
36
36
|
const tenantId = (override ?? event.user.tenantId);
|
|
37
|
-
const
|
|
37
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
38
38
|
// Scoped to the acting user even under a tenant override: a SystemAdmin
|
|
39
39
|
// writing into another tenant still writes their own entry, never
|
|
40
40
|
// someone else's signature.
|
|
@@ -59,7 +59,7 @@ export function makeCollectionSetWrite(collection) {
|
|
|
59
59
|
contentFormat: event.payload.contentFormat,
|
|
60
60
|
folder: event.payload.folder ?? null,
|
|
61
61
|
},
|
|
62
|
-
},
|
|
62
|
+
}, event.user, db, { streamTenantId });
|
|
63
63
|
if (!result.isSuccess)
|
|
64
64
|
return result;
|
|
65
65
|
return {
|
|
@@ -78,7 +78,7 @@ export function makeCollectionSetWrite(collection) {
|
|
|
78
78
|
folder: event.payload.folder ?? null,
|
|
79
79
|
...store.createDefaults(tenantId),
|
|
80
80
|
...owner,
|
|
81
|
-
},
|
|
81
|
+
}, event.user, db, { streamTenantId });
|
|
82
82
|
if (!result.isSuccess)
|
|
83
83
|
return result;
|
|
84
84
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { defineWriteHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
4
5
|
import { TEMPLATE_KINDS, TEXT_BLOCK_KIND } from "../constants.js";
|
|
5
6
|
import { templateResourcesTable } from "../table.js";
|
|
6
7
|
import { contentFormatSchema, executor, folderSchema, localeSchema, slugSchema } from "./shared.js";
|
|
@@ -42,15 +43,11 @@ export const setWrite = defineWriteHandler({
|
|
|
42
43
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
43
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.",
|
|
44
45
|
handler: async (event, ctx) => {
|
|
45
|
-
const db = ctx.db;
|
|
46
46
|
const override = event.payload.tenantIdOverride;
|
|
47
47
|
// @cast-boundary engine-payload — override is a zod-validated string, the
|
|
48
48
|
// user's own tenantId is already TenantId-branded.
|
|
49
49
|
const tenantId = (override ?? event.user.tenantId);
|
|
50
|
-
|
|
51
|
-
// otherwise the event-store stream lookup runs against event.user.tenantId
|
|
52
|
-
// and reports a version conflict although the projection row exists.
|
|
53
|
-
const executorUser = override !== undefined ? { ...event.user, tenantId } : event.user;
|
|
50
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
54
51
|
const kind = event.payload.kind ?? TEXT_BLOCK_KIND;
|
|
55
52
|
const existing = await fetchOne(db, templateResourcesTable, {
|
|
56
53
|
tenantId,
|
|
@@ -87,7 +84,7 @@ export const setWrite = defineWriteHandler({
|
|
|
87
84
|
contentFormat: fields.contentFormat,
|
|
88
85
|
folder: fields.folder,
|
|
89
86
|
},
|
|
90
|
-
},
|
|
87
|
+
}, event.user, db, { streamTenantId });
|
|
91
88
|
if (!result.isSuccess)
|
|
92
89
|
return result;
|
|
93
90
|
return {
|
|
@@ -95,7 +92,9 @@ export const setWrite = defineWriteHandler({
|
|
|
95
92
|
data: { slug: event.payload.slug, locale: event.payload.locale, isNew: false },
|
|
96
93
|
};
|
|
97
94
|
}
|
|
98
|
-
const result = await executor.create({ ...fields, tenantId },
|
|
95
|
+
const result = await executor.create({ ...fields, tenantId }, event.user, db, {
|
|
96
|
+
streamTenantId,
|
|
97
|
+
});
|
|
99
98
|
if (!result.isSuccess)
|
|
100
99
|
return result;
|
|
101
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
|
|
@@ -2,6 +2,7 @@ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
|
2
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,7 +19,6 @@ 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
23
|
// upsertTenant erzeugt scope='tenant'. SYSTEM_TENANT_ID-Override würde
|
|
24
24
|
// scope='tenant' unter SYSTEM_TENANT_ID schreiben → inkonsistenter Zustand
|
|
@@ -34,7 +34,7 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
34
34
|
// event.user.tenantId schon TenantId-branded; union als TenantId casten
|
|
35
35
|
// ist legit (override ist UUID-Format-validiert in schema).
|
|
36
36
|
const tenantId = (override ?? event.user.tenantId);
|
|
37
|
-
const
|
|
37
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
38
38
|
const existing = await fetchOne(db, templateResourcesTable, {
|
|
39
39
|
tenantId,
|
|
40
40
|
slug: event.payload.slug,
|
|
@@ -56,7 +56,7 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
56
56
|
status: event.payload.status,
|
|
57
57
|
};
|
|
58
58
|
if (existing) {
|
|
59
|
-
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 });
|
|
60
60
|
if (!result.isSuccess)
|
|
61
61
|
return result;
|
|
62
62
|
return {
|
|
@@ -64,7 +64,9 @@ export const upsertTenantWrite = defineWriteHandler({
|
|
|
64
64
|
data: { id: String(existing.id), slug: event.payload.slug, isNew: false },
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
const result = await executor.create({ ...fields, tenantId },
|
|
67
|
+
const result = await executor.create({ ...fields, tenantId }, event.user, db, {
|
|
68
|
+
streamTenantId,
|
|
69
|
+
});
|
|
68
70
|
if (!result.isSuccess)
|
|
69
71
|
return result;
|
|
70
72
|
// @cast-boundary db-row — executor.create returnt Record-row aus
|
|
@@ -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,
|
|
@@ -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);
|
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
|
}
|