@cosmicdrift/kumiko-bundled-features 0.377.0 → 0.378.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/cap-counter/stock-cap-guard.js +8 -0
- package/dist/config/web/i18n.js +1 -0
- package/dist/inbound-mail-foundation/handlers/list-accounts.query.d.ts +1 -1
- package/dist/inbound-mail-foundation/handlers/list-accounts.query.js +11 -3
- package/dist/mail-foundation/i18n.d.ts +5 -2
- package/dist/mail-foundation/i18n.js +3 -2
- package/dist/mail-transport-smtp/i18n.d.ts +1 -1
- package/dist/mail-transport-smtp/i18n.js +1 -1
- package/dist/template-resolver/compile-document-body.d.ts +3 -0
- package/dist/template-resolver/compile-document-body.js +50 -3
- package/dist/template-resolver/constants.d.ts +3 -0
- package/dist/template-resolver/constants.js +2 -0
- package/dist/template-resolver/document-blocks.d.ts +2 -2
- package/dist/template-resolver/document-blocks.js +1 -0
- package/dist/template-resolver/document-images.d.ts +2 -0
- package/dist/template-resolver/document-images.js +28 -0
- package/dist/template-resolver/document-sanitizer.d.ts +1 -1
- package/dist/template-resolver/document-sanitizer.js +20 -0
- package/dist/template-resolver/document-schema.d.ts +2 -1
- package/dist/template-resolver/document-schema.js +4 -0
- package/dist/template-resolver/feature.d.ts +9 -0
- package/dist/template-resolver/feature.js +58 -33
- package/dist/template-resolver/handlers/collection-item.query.js +2 -1
- package/dist/template-resolver/handlers/collection-list.query.js +2 -1
- package/dist/template-resolver/handlers/collection-set.write.js +2 -2
- package/dist/template-resolver/handlers/collection-shared.d.ts +1 -2
- package/dist/template-resolver/handlers/collection-shared.js +0 -4
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +2 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +67 -63
- package/dist/template-resolver/handlers/discard-draft.write.d.ts +3 -2
- package/dist/template-resolver/handlers/discard-draft.write.js +18 -6
- package/dist/template-resolver/handlers/import-bundle.write.d.ts +2 -1
- package/dist/template-resolver/handlers/import-bundle.write.js +30 -25
- package/dist/template-resolver/handlers/overview.query.d.ts +2 -1
- package/dist/template-resolver/handlers/overview.query.js +46 -42
- package/dist/template-resolver/handlers/patch-draft.write.d.ts +2 -1
- package/dist/template-resolver/handlers/patch-draft.write.js +92 -86
- package/dist/template-resolver/handlers/reset-to-system.write.d.ts +2 -1
- package/dist/template-resolver/handlers/reset-to-system.write.js +51 -44
- package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +2 -1
- package/dist/template-resolver/handlers/resource-link-pdf.write.js +34 -28
- package/dist/template-resolver/handlers/resource-remove.write.d.ts +2 -1
- package/dist/template-resolver/handlers/resource-remove.write.js +39 -33
- package/dist/template-resolver/handlers/resource-upload.write.d.ts +2 -1
- package/dist/template-resolver/handlers/resource-upload.write.js +104 -96
- package/dist/template-resolver/handlers/send-test-shared.d.ts +5 -0
- package/dist/template-resolver/handlers/send-test-shared.js +19 -0
- package/dist/template-resolver/handlers/send-test-status.query.d.ts +9 -0
- package/dist/template-resolver/handlers/send-test-status.query.js +18 -0
- package/dist/template-resolver/handlers/send-test.write.d.ts +2 -1
- package/dist/template-resolver/handlers/send-test.write.js +87 -89
- package/dist/template-resolver/handlers/set.write.d.ts +2 -1
- package/dist/template-resolver/handlers/set.write.js +94 -90
- package/dist/template-resolver/handlers/toggle-status.write.d.ts +6 -7
- package/dist/template-resolver/handlers/toggle-status.write.js +28 -10
- package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +2 -1
- package/dist/template-resolver/handlers/upsert-tenant.write.js +30 -24
- package/dist/template-resolver/index.d.ts +4 -2
- package/dist/template-resolver/index.js +3 -1
- package/dist/template-resolver/qualified-names.d.ts +2 -0
- package/dist/template-resolver/qualified-names.js +2 -0
- package/dist/template-resolver/template-edit-policy.d.ts +22 -0
- package/dist/template-resolver/template-edit-policy.js +45 -0
- package/dist/template-resolver/template-preview.js +7 -2
- package/dist/template-resolver/template-validation.js +33 -13
- package/dist/template-resolver/web/document-editor-extensions.d.ts +1 -0
- package/dist/template-resolver/web/document-editor-extensions.js +21 -1
- package/dist/template-resolver/web/document-tiptap-editor.d.ts +1 -1
- package/dist/template-resolver/web/document-tiptap-editor.js +22 -6
- package/dist/template-resolver/web/document-toolbar-blocks.d.ts +2 -0
- package/dist/template-resolver/web/i18n.js +4 -0
- package/package.json +8 -8
- package/src/cap-counter/changes.json +12 -0
- package/src/inbound-mail-foundation/changes.json +6 -0
- package/src/mail-foundation/changes.json +6 -0
- package/src/template-resolver/changes.json +20 -0
|
@@ -5,6 +5,7 @@ import * as z from "zod";
|
|
|
5
5
|
import { requireForTenant } from "../../shared/index.js";
|
|
6
6
|
import { TEMPLATE_KINDS, TEXT_BLOCK_KIND, UPSERT_KINDS } from "../constants.js";
|
|
7
7
|
import { templateResourcesTable } from "../table.js";
|
|
8
|
+
import { refuseTemplateEdit, templateEditAccess, } from "../template-edit-policy.js";
|
|
8
9
|
import { resolveTemplateVariables } from "../template-validation.js";
|
|
9
10
|
import { contentFormatSchema, executor, folderSchema, localeSchema, slugSchema } from "./shared.js";
|
|
10
11
|
import { parseStoredJson, storedRecord } from "./template-gates.js";
|
|
@@ -28,100 +29,103 @@ import { gateTemplateWrite } from "./template-write.js";
|
|
|
28
29
|
//
|
|
29
30
|
// Kinds that upsertSystem/upsertTenant also write run the same validation
|
|
30
31
|
// pipeline here, and because saving publishes, publish issues reject too.
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
tenantId
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
65
|
-
const scope = tenantId === SYSTEM_TENANT_ID ? "system" : "tenant";
|
|
66
|
-
let content = event.payload.content;
|
|
67
|
-
if (isOneOf(UPSERT_KINDS, kind)) {
|
|
68
|
-
const gated = gateTemplateWrite({
|
|
32
|
+
export function makeSetWrite(policy) {
|
|
33
|
+
return defineWriteHandler({
|
|
34
|
+
name: "set",
|
|
35
|
+
schema: z.object({
|
|
36
|
+
slug: slugSchema,
|
|
37
|
+
locale: localeSchema,
|
|
38
|
+
/** Which kind to author. Defaults to text-block. */
|
|
39
|
+
kind: z.enum(TEMPLATE_KINDS).optional(),
|
|
40
|
+
title: z.string().min(1).max(200),
|
|
41
|
+
content: z.string().max(200_000).nullable(),
|
|
42
|
+
contentFormat: contentFormatSchema.default("markdown"),
|
|
43
|
+
/** Folder path for the content tree. Optional + null = root node. */
|
|
44
|
+
folder: folderSchema.nullable().optional(),
|
|
45
|
+
/** Optional cross-tenant write — SystemAdmin only. Typical use: the
|
|
46
|
+
* editor lets a SystemAdmin write onto SYSTEM_TENANT_ID, otherwise the
|
|
47
|
+
* text lands on their own platform tenant and the legal routes miss it. */
|
|
48
|
+
tenantIdOverride: z.string().min(1).optional(),
|
|
49
|
+
}),
|
|
50
|
+
access: templateEditAccess(policy.tenantEditable),
|
|
51
|
+
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. A TenantAdmin may write only the slug and kind pairs the app lists in tenantEditable, otherwise SystemAdmin only.",
|
|
52
|
+
handler: async (event, ctx) => {
|
|
53
|
+
const kind = event.payload.kind ?? TEXT_BLOCK_KIND;
|
|
54
|
+
const refused = refuseTemplateEdit(policy, event.user, { slug: event.payload.slug, kind });
|
|
55
|
+
if (refused !== null)
|
|
56
|
+
return refused;
|
|
57
|
+
const override = event.payload.tenantIdOverride;
|
|
58
|
+
// @cast-boundary engine-payload — override is a zod-validated string, the
|
|
59
|
+
// user's own tenantId is already TenantId-branded.
|
|
60
|
+
const tenantId = (override ?? event.user.tenantId);
|
|
61
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
62
|
+
const existing = await fetchOne(db, templateResourcesTable, {
|
|
63
|
+
tenantId,
|
|
64
|
+
slug: event.payload.slug,
|
|
69
65
|
kind,
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
locale: event.payload.locale,
|
|
67
|
+
});
|
|
68
|
+
const scope = tenantId === SYSTEM_TENANT_ID ? "system" : "tenant";
|
|
69
|
+
let content = event.payload.content;
|
|
70
|
+
if (isOneOf(UPSERT_KINDS, kind)) {
|
|
71
|
+
const gated = gateTemplateWrite({
|
|
72
|
+
kind,
|
|
73
|
+
scope,
|
|
74
|
+
contentFormat: event.payload.contentFormat,
|
|
75
|
+
content,
|
|
76
|
+
rawMeta: parseStoredJson(existing?.meta ?? null),
|
|
77
|
+
variables: resolveTemplateVariables(ctx.registry, event.payload.slug, storedRecord(existing?.variableSchema ?? null)),
|
|
78
|
+
}, "active");
|
|
79
|
+
if (!gated.ok)
|
|
80
|
+
return gated.failure;
|
|
81
|
+
content = gated.evaluated.content;
|
|
82
|
+
}
|
|
83
|
+
const fields = {
|
|
84
|
+
slug: event.payload.slug,
|
|
85
|
+
kind,
|
|
86
|
+
locale: event.payload.locale,
|
|
87
|
+
title: event.payload.title,
|
|
72
88
|
content,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
folder: fields.folder,
|
|
108
|
-
},
|
|
109
|
-
}, event.user, db, { streamTenantId });
|
|
89
|
+
contentFormat: event.payload.contentFormat,
|
|
90
|
+
folder: event.payload.folder ?? null,
|
|
91
|
+
variableSchema: "{}",
|
|
92
|
+
linkedResources: "{}",
|
|
93
|
+
scope,
|
|
94
|
+
parentTemplateId: null,
|
|
95
|
+
// Text blocks have no draft stage — saving publishes, so resolveTemplate
|
|
96
|
+
// (which only returns active rows) finds them.
|
|
97
|
+
status: "active",
|
|
98
|
+
};
|
|
99
|
+
if (existing) {
|
|
100
|
+
const result = await executor.update({
|
|
101
|
+
id: existing.id,
|
|
102
|
+
version: existing.version,
|
|
103
|
+
// Only the editable columns — slug/kind/locale are the unique key,
|
|
104
|
+
// and rewriting scope/status/variableSchema here would reset whatever
|
|
105
|
+
// the template upserts put on the row.
|
|
106
|
+
changes: {
|
|
107
|
+
title: fields.title,
|
|
108
|
+
content: fields.content,
|
|
109
|
+
contentFormat: fields.contentFormat,
|
|
110
|
+
folder: fields.folder,
|
|
111
|
+
},
|
|
112
|
+
}, event.user, db, { streamTenantId });
|
|
113
|
+
if (!result.isSuccess)
|
|
114
|
+
return result;
|
|
115
|
+
return {
|
|
116
|
+
isSuccess: true,
|
|
117
|
+
data: { slug: event.payload.slug, locale: event.payload.locale, isNew: false },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const result = await executor.create({ ...fields, tenantId }, event.user, db, {
|
|
121
|
+
streamTenantId,
|
|
122
|
+
});
|
|
110
123
|
if (!result.isSuccess)
|
|
111
124
|
return result;
|
|
112
125
|
return {
|
|
113
126
|
isSuccess: true,
|
|
114
|
-
data: { slug: event.payload.slug, locale: event.payload.locale, isNew:
|
|
127
|
+
data: { slug: event.payload.slug, locale: event.payload.locale, isNew: true },
|
|
115
128
|
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
});
|
|
120
|
-
if (!result.isSuccess)
|
|
121
|
-
return result;
|
|
122
|
-
return {
|
|
123
|
-
isSuccess: true,
|
|
124
|
-
data: { slug: event.payload.slug, locale: event.payload.locale, isNew: true },
|
|
125
|
-
};
|
|
126
|
-
},
|
|
127
|
-
});
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { type HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import type { TemplateResourceRow } from "../table.ts";
|
|
4
|
+
import { type TemplateEditPolicy } from "../template-edit-policy.ts";
|
|
4
5
|
type TemplateStatus = "active" | "archived";
|
|
5
6
|
export type StatusTarget = "own" | "system";
|
|
6
|
-
export declare function writeAccessFor(target: StatusTarget):
|
|
7
|
-
roles: string[];
|
|
8
|
-
};
|
|
7
|
+
export declare function writeAccessFor(target: StatusTarget, policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/handlers").RoleAccessRule;
|
|
9
8
|
export declare function loadTargetRow(ctx: HandlerContext, target: StatusTarget, user: {
|
|
10
9
|
readonly tenantId: string;
|
|
11
10
|
}, id: string): Promise<{
|
|
@@ -13,25 +12,25 @@ export declare function loadTargetRow(ctx: HandlerContext, target: StatusTarget,
|
|
|
13
12
|
scoped: import("@cosmicdrift/kumiko-types/handlers").TenantWriteTarget | null;
|
|
14
13
|
row: TemplateResourceRow | null;
|
|
15
14
|
}>;
|
|
16
|
-
export declare
|
|
15
|
+
export declare function makeArchiveWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
|
|
17
16
|
id: z.ZodString;
|
|
18
17
|
}, z.core.$strip>, {
|
|
19
18
|
id: string;
|
|
20
19
|
status: TemplateStatus;
|
|
21
20
|
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
22
|
-
export declare
|
|
21
|
+
export declare function makePublishWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
|
|
23
22
|
id: z.ZodString;
|
|
24
23
|
}, z.core.$strip>, {
|
|
25
24
|
id: string;
|
|
26
25
|
status: TemplateStatus;
|
|
27
26
|
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
28
|
-
export declare
|
|
27
|
+
export declare function makeArchiveSystemWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
|
|
29
28
|
id: z.ZodString;
|
|
30
29
|
}, z.core.$strip>, {
|
|
31
30
|
id: string;
|
|
32
31
|
status: TemplateStatus;
|
|
33
32
|
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
34
|
-
export declare
|
|
33
|
+
export declare function makePublishSystemWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
|
|
35
34
|
id: z.ZodString;
|
|
36
35
|
}, z.core.$strip>, {
|
|
37
36
|
id: string;
|
|
@@ -5,6 +5,7 @@ import * as z from "zod";
|
|
|
5
5
|
import { requireForTenant } from "../../shared/index.js";
|
|
6
6
|
import { templateResourcesTable } from "../table.js";
|
|
7
7
|
import { activationChanges, parseTemplateDraft, toWorkingRow } from "../template-draft.js";
|
|
8
|
+
import { refuseMissingTemplate, refuseTemplateEdit, templateEditAccess, } from "../template-edit-policy.js";
|
|
8
9
|
import { executor } from "./shared.js";
|
|
9
10
|
import { publishGateIssues } from "./template-gates.js";
|
|
10
11
|
function isRowOfTarget(row, target, callerTenantId) {
|
|
@@ -12,8 +13,10 @@ function isRowOfTarget(row, target, callerTenantId) {
|
|
|
12
13
|
? row.tenantId === SYSTEM_TENANT_ID && row.scope === "system"
|
|
13
14
|
: row.tenantId === callerTenantId && row.scope === "tenant";
|
|
14
15
|
}
|
|
15
|
-
export function writeAccessFor(target) {
|
|
16
|
-
return
|
|
16
|
+
export function writeAccessFor(target, policy) {
|
|
17
|
+
return target === "system"
|
|
18
|
+
? { roles: ["SystemAdmin"] }
|
|
19
|
+
: templateEditAccess(policy.tenantEditable);
|
|
17
20
|
}
|
|
18
21
|
// The tenant db also reads system rows; the row's own tenant must match the
|
|
19
22
|
// target or the executor would append to a foreign event stream.
|
|
@@ -34,18 +37,25 @@ function planStatusChange(ctx, row, status) {
|
|
|
34
37
|
}
|
|
35
38
|
return { ok: true, changes: { ...parseTemplateDraft(row.draft), ...activationChanges(row) } };
|
|
36
39
|
}
|
|
37
|
-
function createStatusUpdateHandler(name, target, status, description) {
|
|
40
|
+
function createStatusUpdateHandler(policy, name, target, status, description) {
|
|
38
41
|
return defineWriteHandler({
|
|
39
42
|
name,
|
|
40
43
|
schema: z.object({ id: z.string().min(1) }),
|
|
41
|
-
access: writeAccessFor(target),
|
|
44
|
+
access: writeAccessFor(target, policy),
|
|
42
45
|
// A system template change reaches every tenant without an override.
|
|
43
46
|
...(target === "system" ? { agent: { risk: "high" } } : {}),
|
|
44
47
|
description,
|
|
45
48
|
handler: async (event, ctx) => {
|
|
46
49
|
const { db, scoped, row } = await loadTargetRow(ctx, target, event.user, event.payload.id);
|
|
47
|
-
if (!row)
|
|
48
|
-
|
|
50
|
+
if (!row) {
|
|
51
|
+
const refused = target === "own" ? refuseMissingTemplate(policy, event.user) : null;
|
|
52
|
+
return refused ?? writeFailure(new NotFoundError("template-resource", event.payload.id));
|
|
53
|
+
}
|
|
54
|
+
if (target === "own") {
|
|
55
|
+
const refused = refuseTemplateEdit(policy, event.user, row);
|
|
56
|
+
if (refused !== null)
|
|
57
|
+
return refused;
|
|
58
|
+
}
|
|
49
59
|
const plan = planStatusChange(ctx, row, status);
|
|
50
60
|
if (!plan.ok)
|
|
51
61
|
return plan.failure;
|
|
@@ -56,7 +66,15 @@ function createStatusUpdateHandler(name, target, status, description) {
|
|
|
56
66
|
},
|
|
57
67
|
});
|
|
58
68
|
}
|
|
59
|
-
export
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
export
|
|
69
|
+
export function makeArchiveWrite(policy) {
|
|
70
|
+
return createStatusUpdateHandler(policy, "archive", "own", "archived", "Sets one template resource of the caller's tenant to archived so the resolver stops serving it while the row and its content survive; use it to retire a template reversibly instead of deleting it. An open draft stays. Outside the templates the app lists in tenantEditable only a SystemAdmin may use it.");
|
|
71
|
+
}
|
|
72
|
+
export function makePublishWrite(policy) {
|
|
73
|
+
return createStatusUpdateHandler(policy, "publish", "own", "active", "Sets one template resource of the caller's tenant to active so the resolver starts serving it; use it to release a draft written by upsert-tenant (a draft saved beside an active version replaces it), or to bring an archived template back; a version with unknown placeholders or unused required variables is refused and the served version stays. Outside the templates the app lists in tenantEditable only a SystemAdmin may use it.");
|
|
74
|
+
}
|
|
75
|
+
export function makeArchiveSystemWrite(policy) {
|
|
76
|
+
return createStatusUpdateHandler(policy, "archive-system", "system", "archived", "Sets one system-default template to archived so the resolver stops serving it to every tenant that has no override; SystemAdmin only, reversible with publish-system.");
|
|
77
|
+
}
|
|
78
|
+
export function makePublishSystemWrite(policy) {
|
|
79
|
+
return createStatusUpdateHandler(policy, "publish-system", "system", "active", "Sets one system-default template to active so the resolver serves it again to every tenant that has no override; an open draft beside the active version replaces it; SystemAdmin only, a version with unknown placeholders or unused required variables is refused.");
|
|
80
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
|
|
2
|
+
import { type TemplateEditPolicy } from "../template-edit-policy.ts";
|
|
3
|
+
export declare function makeUpsertTenantWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"upsert-tenant", z.ZodObject<{
|
|
3
4
|
slug: z.ZodString;
|
|
4
5
|
kind: z.ZodEnum<{
|
|
5
6
|
"ai-prompt": "ai-prompt";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import * as z from "zod";
|
|
3
|
+
import { refuseTemplateEdit, templateEditAccess, } from "../template-edit-policy.js";
|
|
3
4
|
import { resolveTemplateVariables } from "../template-validation.js";
|
|
4
5
|
import { upsertPayloadSchema } from "./shared.js";
|
|
5
6
|
import { writeTemplateRow } from "./template-write.js";
|
|
@@ -8,27 +9,32 @@ import { writeTemplateRow } from "./template-write.js";
|
|
|
8
9
|
// via publish-Handler. SystemAdmin kann via tenantIdOverride für
|
|
9
10
|
// einen anderen Tenant schreiben (typisch: Plattform-Admin-UI das
|
|
10
11
|
// Tenant-Templates kuratiert).
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
tenantId
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
12
|
+
export function makeUpsertTenantWrite(policy) {
|
|
13
|
+
return defineWriteHandler({
|
|
14
|
+
name: "upsert-tenant",
|
|
15
|
+
schema: upsertPayloadSchema.extend({
|
|
16
|
+
tenantIdOverride: z.string().min(1).optional(),
|
|
17
|
+
status: z.enum(["draft", "active"]).default("draft"),
|
|
18
|
+
}),
|
|
19
|
+
access: templateEditAccess(policy.tenantEditable),
|
|
20
|
+
description: "Creates or overwrites a tenant-scoped template override with its variable schema, landing as a draft that only publish makes live (over an active override the draft is stored beside it, the active version keeps being served until publish); it refuses a system-tenant target, which is what upsert-system is for; a TenantAdmin may write it only for templates the app lists in tenantEditable, otherwise SystemAdmin only.",
|
|
21
|
+
handler: async (event, ctx) => {
|
|
22
|
+
const refused = refuseTemplateEdit(policy, event.user, event.payload);
|
|
23
|
+
if (refused !== null)
|
|
24
|
+
return refused;
|
|
25
|
+
// @cast-boundary engine-payload — override is a zod-validated string and
|
|
26
|
+
// event.user.tenantId is already TenantId-branded; forTenant rejects the
|
|
27
|
+
// override for non-SystemAdmins.
|
|
28
|
+
const tenantId = (event.payload.tenantIdOverride ?? event.user.tenantId);
|
|
29
|
+
return writeTemplateRow({
|
|
30
|
+
ctx,
|
|
31
|
+
user: event.user,
|
|
32
|
+
tenantId,
|
|
33
|
+
scope: "tenant",
|
|
34
|
+
status: event.payload.status,
|
|
35
|
+
variables: resolveTemplateVariables(ctx.registry, event.payload.slug, event.payload.variableSchema),
|
|
36
|
+
payload: event.payload,
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
export { isResourceImageContentType } from "../shared/index.ts";
|
|
1
2
|
export { createTemplateResolverApi, type ResolveRequest, requireTemplateResolver, TemplateNotFoundError, type TemplateResolverApi, type TemplateResource, } from "./api.ts";
|
|
2
|
-
export { type CompileDocumentBodyOptions, collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.ts";
|
|
3
|
+
export { type CompileDocumentBodyOptions, collectDocumentImageVariables, collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.ts";
|
|
3
4
|
export { CONTENT_FORMATS, type ContentFormat, DEFAULT_MAIL_LAYOUT_SLUG, DEFAULT_PDF_LAYOUT_SLUG, DOCUMENT_HTML_KINDS, type DocumentHtmlKind, FALLBACK_LOCALE, NON_RENDER_KINDS, type NonRenderKind, PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, type PdfOrientation, type PdfPageFormat, RENDER_KINDS, type RenderKind, SYSTEM_TENANT_ID, TEMPLATE_KINDS, TEMPLATE_SCOPES, TEMPLATE_STATUSES, TEXT_BLOCK_KIND, type TemplateKind, type TemplateScope, type TemplateStatus, UPSERT_KINDS, type UpsertKind, } from "./constants.ts";
|
|
4
5
|
export { type DocumentViolation, type DocumentViolationCode, documentViolationsToFieldIssues, flattenParagraphsToLineBreaks, type SanitizeBodyResult, type SanitizeHtmlResult, sanitizeDocumentHtml, sanitizeTemplateBody, } from "./document-sanitizer.ts";
|
|
5
6
|
export { DOCUMENT_CONDITION_ATTRIBUTE, DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentCondition, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, type DocumentTextAlignment, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.ts";
|
|
6
|
-
export { createTemplateResolverFeature } from "./feature.ts";
|
|
7
|
+
export { createTemplateResolverFeature, type TemplateResolverOptions } from "./feature.ts";
|
|
7
8
|
export type { LayoutSourceResponse, LayoutSourceRow } from "./handlers/layout-source.query.ts";
|
|
8
9
|
export type { OverviewKindGroup, OverviewOrigin, OverviewResponse, OverviewRow, OverviewUsage, } from "./handlers/overview.query.ts";
|
|
9
10
|
export { localeSchema } from "./handlers/shared.ts";
|
|
@@ -16,6 +17,7 @@ export { mapRecordToVariables } from "./record-mapping.ts";
|
|
|
16
17
|
export { type PreparedTemplateImage, prepareTemplateResourceBeforeTransaction, readTemplateResourceMaxBytes, type StoredTemplateResource, storePreparedTemplateResource, } from "./store-template-resource.ts";
|
|
17
18
|
export { type TemplateResourceRow, templateResourceEntity, templateResourcesTable, } from "./table.ts";
|
|
18
19
|
export { type TemplateBundle, templateBundleSchema } from "./template-bundle.ts";
|
|
20
|
+
export { type TemplateEditPolicy, type TenantEditableTemplate, templateEditAccess, } from "./template-edit-policy.ts";
|
|
19
21
|
export { isMailTemplateMeta, type LayoutTemplateMeta, MAIL_CATEGORIES, MAIL_TONES, type MailCategory, type MailLayoutMeta, type MailTemplateMeta, type MailTone, type PdfTemplateMeta, parseTemplateMeta, pdfAttachmentFileNamePatternSchema, readMailLayoutMeta, readPdfTemplateMeta, type TemplateMeta, type TemplateMetaIssue, type TemplateMetaResult, templateSlugSchema, } from "./template-meta.ts";
|
|
20
22
|
export { type DocumentDraftOutcome, evaluateDocumentDraft } from "./template-preview.ts";
|
|
21
23
|
export { readTemplateResource, type TemplateResourceFile } from "./template-resources.ts";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
export { isResourceImageContentType } from "../shared/index.js";
|
|
1
2
|
export { createTemplateResolverApi, requireTemplateResolver, TemplateNotFoundError, } from "./api.js";
|
|
2
|
-
export { collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.js";
|
|
3
|
+
export { collectDocumentImageVariables, collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.js";
|
|
3
4
|
export { CONTENT_FORMATS, DEFAULT_MAIL_LAYOUT_SLUG, DEFAULT_PDF_LAYOUT_SLUG, DOCUMENT_HTML_KINDS, FALLBACK_LOCALE, NON_RENDER_KINDS, PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, RENDER_KINDS, SYSTEM_TENANT_ID, TEMPLATE_KINDS, TEMPLATE_SCOPES, TEMPLATE_STATUSES, TEXT_BLOCK_KIND, UPSERT_KINDS, } from "./constants.js";
|
|
4
5
|
export { documentViolationsToFieldIssues, flattenParagraphsToLineBreaks, sanitizeDocumentHtml, sanitizeTemplateBody, } from "./document-sanitizer.js";
|
|
5
6
|
export { DOCUMENT_CONDITION_ATTRIBUTE, DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentCondition, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
@@ -14,6 +15,7 @@ export { mapRecordToVariables } from "./record-mapping.js";
|
|
|
14
15
|
export { prepareTemplateResourceBeforeTransaction, readTemplateResourceMaxBytes, storePreparedTemplateResource, } from "./store-template-resource.js";
|
|
15
16
|
export { templateResourceEntity, templateResourcesTable, } from "./table.js";
|
|
16
17
|
export { templateBundleSchema } from "./template-bundle.js";
|
|
18
|
+
export { templateEditAccess, } from "./template-edit-policy.js";
|
|
17
19
|
export { isMailTemplateMeta, MAIL_CATEGORIES, MAIL_TONES, parseTemplateMeta, pdfAttachmentFileNamePatternSchema, readMailLayoutMeta, readPdfTemplateMeta, templateSlugSchema, } from "./template-meta.js";
|
|
18
20
|
export { evaluateDocumentDraft } from "./template-preview.js";
|
|
19
21
|
export { readTemplateResource } from "./template-resources.js";
|
|
@@ -31,6 +31,7 @@ export declare const TemplateResolverQueries: {
|
|
|
31
31
|
readonly preview: "template-resolver:query:preview";
|
|
32
32
|
readonly documentBlocks: "template-resolver:query:document-blocks";
|
|
33
33
|
readonly exportBundle: "template-resolver:query:export-bundle";
|
|
34
|
+
readonly sendTestStatus: "template-resolver:query:send-test-status";
|
|
34
35
|
readonly bySlug: "template-resolver:query:by-slug";
|
|
35
36
|
readonly byTenant: "template-resolver:query:by-tenant";
|
|
36
37
|
};
|
|
@@ -47,6 +48,7 @@ export declare const TemplateResolverErrors: {
|
|
|
47
48
|
readonly targetExists: "target_exists";
|
|
48
49
|
readonly systemResourcesNeedSystemTenant: "system_resources_need_system_tenant";
|
|
49
50
|
readonly systemOnlyTemplate: "system_only_template";
|
|
51
|
+
readonly templateNotTenantEditable: "template_not_tenant_editable";
|
|
50
52
|
readonly bundleLocalesDiffer: "bundle_locales_differ";
|
|
51
53
|
readonly resourceTooLarge: "template_resource_too_large";
|
|
52
54
|
readonly resourceUnsupportedImage: "template_resource_unsupported_image";
|
|
@@ -33,6 +33,7 @@ export const TemplateResolverQueries = {
|
|
|
33
33
|
preview: "template-resolver:query:preview",
|
|
34
34
|
documentBlocks: "template-resolver:query:document-blocks",
|
|
35
35
|
exportBundle: "template-resolver:query:export-bundle",
|
|
36
|
+
sendTestStatus: "template-resolver:query:send-test-status",
|
|
36
37
|
// Public pair — anonymous-reachable, kind pinned to text-block.
|
|
37
38
|
bySlug: "template-resolver:query:by-slug",
|
|
38
39
|
byTenant: "template-resolver:query:by-tenant",
|
|
@@ -58,6 +59,7 @@ export const TemplateResolverErrors = {
|
|
|
58
59
|
targetExists: "target_exists",
|
|
59
60
|
systemResourcesNeedSystemTenant: "system_resources_need_system_tenant",
|
|
60
61
|
systemOnlyTemplate: "system_only_template",
|
|
62
|
+
templateNotTenantEditable: "template_not_tenant_editable",
|
|
61
63
|
bundleLocalesDiffer: "bundle_locales_differ",
|
|
62
64
|
resourceTooLarge: "template_resource_too_large",
|
|
63
65
|
resourceUnsupportedImage: "template_resource_unsupported_image",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type HandlerContext, type RoleAccessRule, type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { type WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import type { TemplateKind } from "./constants.ts";
|
|
4
|
+
export type TenantEditableTemplate = {
|
|
5
|
+
readonly kind: TemplateKind;
|
|
6
|
+
readonly slug: string;
|
|
7
|
+
readonly access: RoleAccessRule;
|
|
8
|
+
};
|
|
9
|
+
export type TemplateEditPolicy = {
|
|
10
|
+
readonly tenantEditable: readonly TenantEditableTemplate[];
|
|
11
|
+
};
|
|
12
|
+
type EditTarget = {
|
|
13
|
+
readonly slug: string;
|
|
14
|
+
readonly kind: string;
|
|
15
|
+
};
|
|
16
|
+
type EditUser = Pick<SessionUser, "roles">;
|
|
17
|
+
export declare function templateEditAccess(entries: readonly TenantEditableTemplate[]): RoleAccessRule;
|
|
18
|
+
export declare function mayEditTemplate(policy: TemplateEditPolicy, user: EditUser, target: EditTarget): boolean;
|
|
19
|
+
export declare function refuseTemplateEdit(policy: TemplateEditPolicy, user: EditUser, target: EditTarget): WriteFailure | null;
|
|
20
|
+
export declare function refuseMissingTemplate(policy: TemplateEditPolicy, user: EditUser): WriteFailure | null;
|
|
21
|
+
export declare function refuseTemplateEditById(db: HandlerContext["db"], policy: TemplateEditPolicy, user: EditUser, tenantId: string, templateId: string): Promise<WriteFailure | null>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { hasAccess, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { AccessDeniedError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import { TemplateResolverErrors } from "./qualified-names.js";
|
|
5
|
+
import { templateResourcesTable } from "./table.js";
|
|
6
|
+
import { isSystemAdmin } from "./template-draft.js";
|
|
7
|
+
const SYSTEM_ADMIN_ROLE = "SystemAdmin";
|
|
8
|
+
export function templateEditAccess(entries) {
|
|
9
|
+
const roles = new Set([SYSTEM_ADMIN_ROLE]);
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
for (const role of entry.access.roles)
|
|
12
|
+
roles.add(role);
|
|
13
|
+
}
|
|
14
|
+
return { roles: [...roles] };
|
|
15
|
+
}
|
|
16
|
+
export function mayEditTemplate(policy, user, target) {
|
|
17
|
+
if (isSystemAdmin(user))
|
|
18
|
+
return true;
|
|
19
|
+
const entry = policy.tenantEditable.find((candidate) => candidate.slug === target.slug && candidate.kind === target.kind);
|
|
20
|
+
return entry !== undefined && hasAccess(user, entry.access);
|
|
21
|
+
}
|
|
22
|
+
export function refuseTemplateEdit(policy, user, target) {
|
|
23
|
+
if (mayEditTemplate(policy, user, target))
|
|
24
|
+
return null;
|
|
25
|
+
return writeFailure(new AccessDeniedError({
|
|
26
|
+
i18nKey: "templateResolver.errors.templateNotTenantEditable",
|
|
27
|
+
details: { reason: TemplateResolverErrors.templateNotTenantEditable },
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
// Fail closed: a missing row gives a non-SystemAdmin the same refusal as a locked one,
|
|
31
|
+
// so the response never reveals whether an id exists. SystemAdmin keeps the 404.
|
|
32
|
+
export function refuseMissingTemplate(policy, user) {
|
|
33
|
+
return isSystemAdmin(user) ? null : refuseTemplateEdit(policy, user, { slug: "", kind: "" });
|
|
34
|
+
}
|
|
35
|
+
export async function refuseTemplateEditById(db, policy, user, tenantId, templateId) {
|
|
36
|
+
if (isSystemAdmin(user))
|
|
37
|
+
return null;
|
|
38
|
+
const row = await fetchOne(db, templateResourcesTable, {
|
|
39
|
+
tenantId,
|
|
40
|
+
id: templateId,
|
|
41
|
+
});
|
|
42
|
+
return row === undefined
|
|
43
|
+
? refuseMissingTemplate(policy, user)
|
|
44
|
+
: refuseTemplateEdit(policy, user, row);
|
|
45
|
+
}
|
|
@@ -64,12 +64,17 @@ function sampleList(def, labelText) {
|
|
|
64
64
|
rows.push(sampleRow(def.item, labelText));
|
|
65
65
|
return rows;
|
|
66
66
|
}
|
|
67
|
+
function sampleValueFor(name, def, labelText) {
|
|
68
|
+
// An images variable has no sample: file ids are record data, never preview content.
|
|
69
|
+
if (def.type === "images")
|
|
70
|
+
return [];
|
|
71
|
+
return def.type === "list" ? sampleList(def, labelText) : sampleScalar(name, def, labelText);
|
|
72
|
+
}
|
|
67
73
|
// Declared examples win; the fallbacks are deterministic so a preview never differs between calls.
|
|
68
74
|
export function sampleValuesFor(variables, labelText = (label) => label) {
|
|
69
75
|
const values = {};
|
|
70
76
|
for (const [name, def] of Object.entries(variables)) {
|
|
71
|
-
values[name] =
|
|
72
|
-
def.type === "list" ? sampleList(def, labelText) : sampleScalar(name, def, labelText);
|
|
77
|
+
values[name] = sampleValueFor(name, def, labelText);
|
|
73
78
|
}
|
|
74
79
|
return values;
|
|
75
80
|
}
|