@cosmicdrift/kumiko-bundled-features 0.387.0 → 0.388.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/tool-dispatch.d.ts +7 -7
- package/dist/agent-tools/tool-dispatch.js +9 -9
- package/dist/audit/handlers/audit-events.js +1 -1
- package/dist/auth-mfa/feature-exports.d.ts +2 -2
- package/dist/auth-mfa/feature.js +8 -0
- package/dist/auth-mfa/web/i18n.js +5 -1
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.d.ts +11 -0
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.js +12 -0
- package/dist/auth-mfa/web/mfa-disable-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-regenerate-recovery-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +10 -5
- package/dist/auth-mfa/web/mfa-verify-screen.js +26 -5
- package/dist/billing-foundation/checkout-core.d.ts +9 -14
- package/dist/billing-foundation/checkout-core.js +24 -39
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +8 -2
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +1 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +9 -3
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +3 -1
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +4 -2
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +6 -5
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +5 -1
- package/dist/billing-foundation/consumer-protection/termination-record.js +27 -17
- package/dist/billing-foundation/consumer-protection/termination-request.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-revoke.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -0
- package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +5 -2
- package/dist/billing-foundation/consumer-protection/withdrawal-period.js +8 -9
- package/dist/billing-foundation/events.d.ts +1 -0
- package/dist/billing-foundation/events.js +1 -0
- package/dist/billing-foundation/feature.js +3 -1
- package/dist/billing-foundation/handlers/billing-plans.query.js +35 -5
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +1 -0
- package/dist/billing-foundation/handlers/plan-switch-preview.query.d.ts +13 -0
- package/dist/billing-foundation/handlers/plan-switch-preview.query.js +48 -0
- package/dist/billing-foundation/handlers/process-event.write.d.ts +6 -0
- package/dist/billing-foundation/handlers/process-event.write.js +8 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +32 -5
- package/dist/billing-foundation/handlers/switch-plan.write.d.ts +2 -2
- package/dist/billing-foundation/handlers/switch-plan.write.js +56 -64
- package/dist/billing-foundation/handlers/sync-subscription.write.js +12 -1
- package/dist/billing-foundation/i18n.js +22 -0
- package/dist/billing-foundation/index.d.ts +1 -1
- package/dist/billing-foundation/plan-catalog.d.ts +21 -5
- package/dist/billing-foundation/plan-catalog.js +25 -16
- package/dist/billing-foundation/plan-switch.d.ts +12 -0
- package/dist/billing-foundation/plan-switch.js +60 -0
- package/dist/billing-foundation/types.d.ts +57 -1
- package/dist/billing-foundation/types.js +9 -0
- package/dist/billing-foundation/validate-options.js +17 -0
- package/dist/billing-foundation/web/billing-plans-panel.js +78 -32
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +17 -3
- package/dist/billing-foundation/web/checkout-consent-dialog.js +14 -10
- package/dist/billing-foundation/web/constants.d.ts +4 -0
- package/dist/billing-foundation/web/constants.js +4 -0
- package/dist/billing-foundation/web/plan-price.d.ts +15 -0
- package/dist/billing-foundation/web/plan-price.js +32 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.d.ts +15 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.js +66 -0
- package/dist/cap-counter/cap-reservation.js +5 -1
- package/dist/cap-counter/enforce-cap.js +2 -1
- package/dist/cap-counter/stock-cap-guard.js +7 -1
- package/dist/channel-email/feature.js +1 -1
- package/dist/config/handlers/cascade.query.d.ts +5 -0
- package/dist/config/handlers/cascade.query.js +4 -3
- package/dist/config/handlers/values.query.d.ts +5 -0
- package/dist/config/handlers/values.query.js +4 -3
- package/dist/config/write-helpers.d.ts +1 -0
- package/dist/config/write-helpers.js +18 -9
- package/dist/crypto-shredding/handlers/forget-subject.write.js +13 -10
- package/dist/delivery/channel-context.js +6 -3
- package/dist/delivery/db/queries/preferences.js +2 -2
- package/dist/delivery/delivery-service.js +22 -7
- package/dist/delivery/handlers/preferences.query.d.ts +1 -1
- package/dist/delivery/handlers/preferences.query.js +20 -1
- package/dist/delivery/handlers/unsubscribe-page-chrome.query.js +1 -1
- package/dist/delivery/unsubscribe-link.d.ts +3 -1
- package/dist/delivery/unsubscribe-page-strings.d.ts +1 -0
- package/dist/delivery/unsubscribe-page-strings.js +1 -0
- package/dist/delivery/unsubscribe-page.d.ts +2 -0
- package/dist/delivery/unsubscribe-page.js +5 -0
- package/dist/delivery/unsubscribe.js +25 -7
- package/dist/file-foundation/index.d.ts +1 -1
- package/dist/file-foundation/index.js +1 -1
- package/dist/file-foundation/read-file-ref-content.d.ts +3 -0
- package/dist/file-foundation/read-file-ref-content.js +6 -1
- package/dist/file-provider-s3/env-named-feature.d.ts +0 -2
- package/dist/form-draft/db/queries/cleanup.js +2 -2
- package/dist/form-draft/decrypt-draft.js +5 -1
- package/dist/host-routing/dispatch.js +11 -20
- package/dist/inbound-mail-foundation/handlers/update-account.write.d.ts +2 -2
- package/dist/inbound-mail-foundation/handlers/update-account.write.js +12 -4
- package/dist/inbound-mail-foundation/i18n.js +0 -1
- package/dist/inbound-mail-foundation/screens.js +0 -3
- package/dist/inbound-mail-foundation/types.d.ts +4 -3
- package/dist/inbound-mail-foundation/watch-supervisor.js +28 -11
- package/dist/jobs/feature.d.ts +1 -0
- package/dist/jobs/feature.js +8 -4
- package/dist/jobs/handlers/tenant-failures.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-failures.query.js +45 -41
- package/dist/jobs/handlers/tenant-runs.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-runs.query.js +54 -50
- package/dist/jobs/job-run-logger.js +5 -4
- package/dist/mail-foundation/feature.d.ts +3 -3
- package/dist/mail-foundation/feature.js +3 -0
- package/dist/personal-access-tokens/feature.d.ts +1 -1
- package/dist/personal-access-tokens/feature.js +4 -0
- package/dist/renderer-pdf/render-table-pdf.d.ts +1 -1
- package/dist/renderer-pdf/render-table-pdf.js +22 -10
- package/dist/sessions/feature.d.ts +2 -2
- package/dist/sessions/feature.js +4 -0
- package/dist/shared/decrypt-stored-pii.d.ts +1 -0
- package/dist/shared/decrypt-stored-pii.js +6 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/mail-template-body.d.ts +6 -2
- package/dist/shared/mail-template-body.js +7 -4
- package/dist/step-dispatcher/feature.js +1 -0
- package/dist/subscription-dodo/client.d.ts +17 -0
- package/dist/subscription-dodo/client.js +31 -0
- package/dist/subscription-dodo/constants.d.ts +12 -0
- package/dist/subscription-dodo/constants.js +19 -0
- package/dist/subscription-dodo/feature.d.ts +37 -0
- package/dist/subscription-dodo/feature.js +144 -0
- package/dist/subscription-dodo/index.d.ts +2 -0
- package/dist/subscription-dodo/index.js +4 -0
- package/dist/subscription-dodo/plugin-methods.d.ts +23 -0
- package/dist/subscription-dodo/plugin-methods.js +260 -0
- package/dist/subscription-dodo/runtime.d.ts +27 -0
- package/dist/subscription-dodo/runtime.js +89 -0
- package/dist/subscription-dodo/verify-webhook.d.ts +37 -0
- package/dist/subscription-dodo/verify-webhook.js +189 -0
- package/dist/subscription-stripe/feature.js +1 -0
- package/dist/subscription-stripe/runtime.js +7 -6
- package/dist/template-resolver/api.d.ts +3 -2
- package/dist/template-resolver/api.js +24 -2
- package/dist/template-resolver/compile-document-body.js +1 -0
- package/dist/template-resolver/feature.js +2 -2
- package/dist/template-resolver/handlers/bundle-import.js +3 -3
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +5 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +22 -7
- package/dist/template-resolver/handlers/diff-to-system.query.js +3 -2
- package/dist/template-resolver/handlers/document-base.js +3 -2
- package/dist/template-resolver/handlers/find-by-id.query.js +3 -2
- package/dist/template-resolver/handlers/list.query.js +5 -6
- package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
- package/dist/template-resolver/handlers/overview.query.js +6 -3
- package/dist/template-resolver/handlers/tenant-override-counts.query.js +1 -3
- package/dist/template-resolver/handlers/tenant-override-selection.d.ts +2 -1
- package/dist/template-resolver/handlers/tenant-overrides.query.js +28 -5
- package/dist/template-resolver/handlers/usage.query.d.ts +2 -1
- package/dist/template-resolver/handlers/usage.query.js +26 -16
- package/dist/template-resolver/handlers/validate.query.js +7 -2
- package/dist/template-resolver/seeding.js +26 -10
- package/dist/template-resolver/store-template-resource.d.ts +1 -0
- package/dist/template-resolver/store-template-resource.js +19 -4
- package/dist/template-resolver/template-delivery-policy.d.ts +0 -5
- package/dist/template-resolver/template-delivery-policy.js +0 -6
- package/dist/template-resolver/template-meta.js +7 -4
- package/dist/template-resolver/template-preview.js +4 -3
- package/dist/template-resolver/web/constants.d.ts +2 -2
- package/dist/template-resolver/web/constants.js +3 -3
- package/dist/template-resolver/web/index.d.ts +1 -0
- package/dist/template-resolver/web/index.js +1 -0
- package/dist/template-resolver/web/mail-layout-constants.d.ts +5 -0
- package/dist/template-resolver/web/mail-layout-constants.js +7 -0
- package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/member-directory.query.js +10 -3
- package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/tenant-directory.query.js +8 -4
- package/dist/tenant-lifecycle/constants.d.ts +1 -0
- package/dist/tenant-lifecycle/constants.js +4 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +2 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -1
- package/dist/tier-engine/web/constants.js +2 -2
- package/dist/user-data-rights/run-export-jobs.js +3 -4
- package/dist/user-data-rights/run-forget-cleanup.js +2 -1
- package/dist/waitlist/handlers/export-purge.write.js +2 -1
- package/dist/waitlist/handlers/submit.write.js +5 -5
- package/dist/waitlist/import.d.ts +1 -0
- package/dist/waitlist/import.js +4 -0
- package/dist/waitlist/legacy-migrate.js +17 -6
- package/dist/waitlist/link-on-signup.js +18 -6
- package/dist/waitlist/options.d.ts +3 -0
- package/package.json +19 -9
- package/src/agent-tools/changes.json +6 -0
- package/src/audit/changes.json +12 -0
- package/src/auth-mfa/changes.json +6 -0
- package/src/billing-foundation/changes.json +55 -0
- package/src/cap-counter/changes.json +18 -0
- package/src/channel-email/changes.json +6 -0
- package/src/config/changes.json +12 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/delivery/changes.json +30 -0
- package/src/form-draft/changes.json +6 -0
- package/src/host-routing/changes.json +6 -0
- package/src/inbound-mail-foundation/changes.json +18 -0
- package/src/jobs/changes.json +13 -0
- package/src/mail-foundation/changes.json +6 -0
- package/src/renderer-pdf/changes.json +6 -0
- package/src/step-dispatcher/changes.json +6 -0
- package/src/subscription-dodo/changes.json +14 -0
- package/src/subscription-stripe/changes.json +12 -0
- package/src/template-resolver/changes.json +48 -0
- package/src/tenant/changes.json +6 -0
- package/src/tenant-lifecycle/changes.json +6 -0
- package/src/tier-engine/changes.json +6 -0
- package/src/waitlist/changes.json +18 -0
- package/dist/billing-foundation/consumer-protection/stored-user-email.d.ts +0 -1
- package/dist/billing-foundation/consumer-protection/stored-user-email.js +0 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { defineQueryHandler, MAX_LIST_LIMIT, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { TenantQueries } from "../../tenant/index.js";
|
|
5
5
|
import { templateResourcesTable } from "../table.js";
|
|
@@ -10,8 +10,31 @@ import { loadEnabledTenantIds, pickBestRowPerTenant, TENANT_OVERRIDES_REASON, }
|
|
|
10
10
|
const directorySchema = z.object({
|
|
11
11
|
rows: z.array(z.object({ id: z.string(), label: z.string() })),
|
|
12
12
|
});
|
|
13
|
-
function
|
|
14
|
-
|
|
13
|
+
function chunksOf(items, size) {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
for (let start = 0; start < items.length; start += size) {
|
|
16
|
+
chunks.push(items.slice(start, start + size));
|
|
17
|
+
}
|
|
18
|
+
return chunks;
|
|
19
|
+
}
|
|
20
|
+
// Looks up only the ids the overrides need, so labels never depend on the directory's default cap.
|
|
21
|
+
async function labelsFor(ctx, queryName, ids) {
|
|
22
|
+
const labels = new Map();
|
|
23
|
+
for (const chunk of chunksOf([...ids], MAX_LIST_LIMIT)) {
|
|
24
|
+
const raw = await ctx.query(queryName, { ids: chunk });
|
|
25
|
+
for (const row of directorySchema.parse(raw).rows)
|
|
26
|
+
labels.set(row.id, row.label);
|
|
27
|
+
}
|
|
28
|
+
return labels;
|
|
29
|
+
}
|
|
30
|
+
function changerIdsOf(rows) {
|
|
31
|
+
const ids = new Set();
|
|
32
|
+
for (const row of rows) {
|
|
33
|
+
const changedById = row.modifiedById ?? row.insertedById;
|
|
34
|
+
if (changedById !== null)
|
|
35
|
+
ids.add(changedById);
|
|
36
|
+
}
|
|
37
|
+
return ids;
|
|
15
38
|
}
|
|
16
39
|
function statusOf(row) {
|
|
17
40
|
return row.status === "active" ? "active" : "draft";
|
|
@@ -55,10 +78,10 @@ export const tenantOverridesQuery = defineQueryHandler({
|
|
|
55
78
|
const bestByTenant = pickBestRowPerTenant(found, enabledTenantIds);
|
|
56
79
|
const best = [...bestByTenant.values()];
|
|
57
80
|
const tenantLabels = hasTenantFeature
|
|
58
|
-
?
|
|
81
|
+
? await labelsFor(ctx, TenantQueries.tenantDirectory, new Set(best.map((row) => row.tenantId)))
|
|
59
82
|
: new Map();
|
|
60
83
|
const userLabels = hasTenantFeature
|
|
61
|
-
?
|
|
84
|
+
? await labelsFor(ctx, TenantQueries.memberDirectory, changerIdsOf(best))
|
|
62
85
|
: new Map();
|
|
63
86
|
const rows = best
|
|
64
87
|
.map((row) => toOverrideRow(row, tenantLabels, userLabels))
|
|
@@ -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 makeUsageQuery(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"usage", z.ZodObject<{
|
|
3
4
|
slug: z.ZodString;
|
|
4
5
|
kind: z.ZodEnum<{
|
|
5
6
|
"ai-prompt": "ai-prompt";
|
|
@@ -6,20 +6,30 @@ import { deliveryPolicyOf } from "../template-delivery-policy.js";
|
|
|
6
6
|
import { templateSlugSchema } from "../template-meta.js";
|
|
7
7
|
import { collectTemplateUsage } from "../template-usage.js";
|
|
8
8
|
import { TEMPLATE_KINDS } from "../web/constants.js";
|
|
9
|
-
import { effectiveMailLayoutSlugs } from "./overview.query.js";
|
|
9
|
+
import { effectiveMailLayoutSlugs, isRowVisibleToCaller } from "./overview.query.js";
|
|
10
10
|
const MAIL_ROW_LIMIT = 5000;
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
11
|
+
export function makeUsageQuery(policy) {
|
|
12
|
+
return defineQueryHandler({
|
|
13
|
+
name: "usage",
|
|
14
|
+
schema: z.object({ slug: templateSlugSchema, kind: z.enum(TEMPLATE_KINDS) }),
|
|
15
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
16
|
+
description: "Lists the code-registered notifications that use one template: a mail template is used by the notification whose qualified name is its slug and that has a code template, a mail layout by every mail notification whose effective template (own override, else system) names it, or whose code meta does when no stored template exists, or that falls back to the default layout; use it to show what a change to a template affects.",
|
|
17
|
+
handler: async (query, ctx) => {
|
|
18
|
+
const { slug, kind } = query.payload;
|
|
19
|
+
if (kind !== "mail-layout")
|
|
20
|
+
return { usedBy: collectTemplateUsage(ctx.registry, slug, kind) };
|
|
21
|
+
const fetchedRows = await selectMany(ctx.db, templateResourcesTable, { kind: "mail" }, { orderBy: { col: "id", direction: "asc" }, limit: MAIL_ROW_LIMIT + 1 });
|
|
22
|
+
if (fetchedRows.length > MAIL_ROW_LIMIT) {
|
|
23
|
+
ctx.log?.warn("template usage: row limit reached, result is incomplete", {
|
|
24
|
+
limit: MAIL_ROW_LIMIT,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const visibleRows = fetchedRows
|
|
28
|
+
.slice(0, MAIL_ROW_LIMIT)
|
|
29
|
+
.filter((row) => isRowVisibleToCaller(policy, query.user, ctx.registry, row));
|
|
30
|
+
return {
|
|
31
|
+
usedBy: collectTemplateUsage(ctx.registry, slug, kind, effectiveMailLayoutSlugs(visibleRows, deliveryPolicyOf(ctx.registry))),
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { defineQueryHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { isRowDeliveredTo } from "../api.js";
|
|
4
5
|
import { imageResourceKeysOf } from "../linked-resources.js";
|
|
5
6
|
import { templateResourcesTable } from "../table.js";
|
|
6
|
-
import { deliveryPolicyOf
|
|
7
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.js";
|
|
7
8
|
import { isSystemAdmin } from "../template-draft.js";
|
|
8
9
|
import { resolveTemplateVariables, systemPlaceholdersFor } from "../template-validation.js";
|
|
9
10
|
import { contentFormatSchema, kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
|
|
@@ -31,7 +32,11 @@ export async function knownResourceKeysFor(ctx, user, where) {
|
|
|
31
32
|
locale,
|
|
32
33
|
})),
|
|
33
34
|
...(scope === "tenant" &&
|
|
34
|
-
|
|
35
|
+
(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), user, {
|
|
36
|
+
tenantId: user.tenantId,
|
|
37
|
+
kind,
|
|
38
|
+
slug,
|
|
39
|
+
}))
|
|
35
40
|
? await knownResourceKeys(ctx.db, { tenantId: user.tenantId, slug, kind, scope, locale })
|
|
36
41
|
: []),
|
|
37
42
|
];
|
|
@@ -39,10 +39,16 @@ async function resolveExistingForEventStoreSeed(db, tdb, existingRow, tenantId)
|
|
|
39
39
|
}
|
|
40
40
|
return { id, version: streamVersion };
|
|
41
41
|
}
|
|
42
|
+
// Namespace prefix plus path, no whitespace: keeps free text from passing as a key that the UI
|
|
43
|
+
// would then show raw.
|
|
44
|
+
const I18N_KEY_FORMAT = /^[^\s:]+:\S+$/;
|
|
42
45
|
function seedTitleColumn(opts, label) {
|
|
43
46
|
if (opts.title !== undefined && opts.titleI18nKey !== undefined) {
|
|
44
47
|
throw new Error(`${label} rejected: set either title or titleI18nKey, not both`);
|
|
45
48
|
}
|
|
49
|
+
if (opts.titleI18nKey !== undefined && !I18N_KEY_FORMAT.test(opts.titleI18nKey)) {
|
|
50
|
+
throw new Error(`${label} rejected: titleI18nKey "${opts.titleI18nKey}" is not a "namespace:key" i18n key`);
|
|
51
|
+
}
|
|
46
52
|
return opts.titleI18nKey ?? opts.title ?? null;
|
|
47
53
|
}
|
|
48
54
|
export async function seedSystemTemplate(db, opts) {
|
|
@@ -101,16 +107,18 @@ export async function seedSystemTemplate(db, opts) {
|
|
|
101
107
|
current?.title === rowFields.title &&
|
|
102
108
|
current?.variableSchema === rowFields.variableSchema &&
|
|
103
109
|
current?.meta === rowFields.meta &&
|
|
104
|
-
current?.status === rowFields.status &&
|
|
105
110
|
current?.parentTemplateId === rowFields.parentTemplateId &&
|
|
106
111
|
(linkedResources === undefined || current?.linkedResources === linkedResources)) {
|
|
107
112
|
return { id: String(row.id) };
|
|
108
113
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
// An archived system row is a SystemAdmin decision (reversible with publish-system) that a
|
|
115
|
+
// pod-boot re-seed must not undo; content still follows the code. The seed replaces the
|
|
116
|
+
// content, so an open draft on the old content is dropped with it.
|
|
117
|
+
const { status: _status, ...contentFields } = rowFields;
|
|
118
|
+
const changes = current?.status === "archived"
|
|
119
|
+
? { ...contentFields, status: "archived", draft: null }
|
|
120
|
+
: { ...rowFields, ...activeStampChanges(row), draft: null };
|
|
121
|
+
const result = await executor.update({ id: row.id, version: row.version, changes }, by, tdb);
|
|
114
122
|
if (!result.isSuccess) {
|
|
115
123
|
throw new Error(`seedSystemTemplate update failed: ${JSON.stringify(result)}`);
|
|
116
124
|
}
|
|
@@ -191,8 +199,10 @@ async function seedSystemTemplateBundleWithResources(db, bundle, opts) {
|
|
|
191
199
|
if (!dispatcher) {
|
|
192
200
|
throw new Error(`${label}: bundles with resources need a file provider; pass opts.dispatcher (the dispatcher of the seed deps) so the files are stored through the import-bundle-system write`);
|
|
193
201
|
}
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
const skipExisting = (opts.ifExists ?? "skip") === "skip";
|
|
203
|
+
const existingIds = {};
|
|
204
|
+
let bundleToImport = bundle;
|
|
205
|
+
if (skipExisting) {
|
|
196
206
|
for (const locale of Object.keys(bundle.locales)) {
|
|
197
207
|
const row = await fetchOne(db, templateResourcesTable, {
|
|
198
208
|
tenantId: SYSTEM_TENANT_ID,
|
|
@@ -206,12 +216,18 @@ async function seedSystemTemplateBundleWithResources(db, bundle, opts) {
|
|
|
206
216
|
if (Object.keys(existingIds).length === Object.keys(bundle.locales).length) {
|
|
207
217
|
return { ids: existingIds };
|
|
208
218
|
}
|
|
219
|
+
// import-bundle-system always overwrites, so only the missing locales go in: a
|
|
220
|
+
// hand-edited system row of an already seeded locale must survive the new-locale boot.
|
|
221
|
+
bundleToImport = {
|
|
222
|
+
...bundle,
|
|
223
|
+
locales: Object.fromEntries(Object.entries(bundle.locales).filter(([locale]) => !(locale in existingIds))),
|
|
224
|
+
};
|
|
209
225
|
}
|
|
210
|
-
const result = await dispatcher.write(TemplateResolverHandlers.importBundleSystem, { bundle }, opts.by ?? createSystemUser(SYSTEM_TENANT_ID, ["SystemAdmin"]));
|
|
226
|
+
const result = await dispatcher.write(TemplateResolverHandlers.importBundleSystem, { bundle: bundleToImport }, opts.by ?? createSystemUser(SYSTEM_TENANT_ID, ["SystemAdmin"]));
|
|
211
227
|
if (!result.isSuccess) {
|
|
212
228
|
throw new Error(`${label} failed: ${result.error.code}: ${result.error.message}`);
|
|
213
229
|
}
|
|
214
|
-
return { ids: importedIds(result.data) };
|
|
230
|
+
return { ids: { ...existingIds, ...importedIds(result.data) } };
|
|
215
231
|
}
|
|
216
232
|
function importedIds(data) {
|
|
217
233
|
const ids = isPlainObject(data) ? data["ids"] : undefined;
|
|
@@ -42,6 +42,7 @@ export declare function prepareTemplateResourceBeforeTransaction(args: {
|
|
|
42
42
|
}): Promise<BeforeTransactionResult<PreparedTemplateImage>>;
|
|
43
43
|
export type Release = () => Promise<void>;
|
|
44
44
|
export declare function releaseAll(releases: readonly Release[]): Promise<void>;
|
|
45
|
+
export declare function releaseAfterFailure(ctx: HandlerContext, releases: readonly Release[]): Promise<void>;
|
|
45
46
|
export type PreparedLinkedResourceCopy = {
|
|
46
47
|
readonly key: string;
|
|
47
48
|
readonly entry: LinkedResourceEntry;
|
|
@@ -24,6 +24,9 @@ function keyInUse() {
|
|
|
24
24
|
details: { code: "key_in_use" },
|
|
25
25
|
}));
|
|
26
26
|
}
|
|
27
|
+
function isImageKey(links, key) {
|
|
28
|
+
return typeof links[key] === "string";
|
|
29
|
+
}
|
|
27
30
|
function isAttachmentKey(links, key) {
|
|
28
31
|
const occupant = links[key];
|
|
29
32
|
return occupant !== undefined && typeof occupant !== "string";
|
|
@@ -129,6 +132,17 @@ export async function releaseAll(releases) {
|
|
|
129
132
|
if (rejected?.status === "rejected")
|
|
130
133
|
throw rejected.reason;
|
|
131
134
|
}
|
|
135
|
+
// Failure paths: a failing release must not replace the upload error the caller needs to see.
|
|
136
|
+
export async function releaseAfterFailure(ctx, releases) {
|
|
137
|
+
try {
|
|
138
|
+
await releaseAll(releases);
|
|
139
|
+
}
|
|
140
|
+
catch (releaseError) {
|
|
141
|
+
ctx.log?.warn("template upload rollback: releasing stored files failed", {
|
|
142
|
+
error: releaseError instanceof Error ? releaseError.message : String(releaseError),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
132
146
|
// beforeTransaction half: copies the file behind every entry into the target tenant's storage, so a
|
|
133
147
|
// template copied from the system default keeps working once the tenant exports it or measures its
|
|
134
148
|
// attachments. An entry whose source file is gone is left out: it could not be resolved at render
|
|
@@ -148,7 +162,7 @@ export async function prepareLinkedResourceCopiesBeforeTransaction(args) {
|
|
|
148
162
|
continue;
|
|
149
163
|
const uploaded = await uploadToTenant({ ctx, user, tenantId }, { fileName: file.fileName, mimeType: file.contentType, data: file.bytes });
|
|
150
164
|
if (!uploaded.isSuccess) {
|
|
151
|
-
await
|
|
165
|
+
await releaseAfterFailure(ctx, releases);
|
|
152
166
|
return uploaded;
|
|
153
167
|
}
|
|
154
168
|
if (uploaded.release)
|
|
@@ -157,7 +171,7 @@ export async function prepareLinkedResourceCopiesBeforeTransaction(args) {
|
|
|
157
171
|
}
|
|
158
172
|
}
|
|
159
173
|
catch (cause) {
|
|
160
|
-
await
|
|
174
|
+
await releaseAfterFailure(ctx, releases);
|
|
161
175
|
throw cause;
|
|
162
176
|
}
|
|
163
177
|
return { isSuccess: true, data: copies, release: () => releaseAll(releases) };
|
|
@@ -275,8 +289,7 @@ async function checkAttachmentTarget(args) {
|
|
|
275
289
|
if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
|
|
276
290
|
return attachmentLimitFailure();
|
|
277
291
|
}
|
|
278
|
-
|
|
279
|
-
if (occupant !== undefined && typeof occupant === "string")
|
|
292
|
+
if (isImageKey(linked, key))
|
|
280
293
|
return keyInUse();
|
|
281
294
|
return { isSuccess: true, data: { row, linked } };
|
|
282
295
|
}
|
|
@@ -387,6 +400,8 @@ export async function linkTemplatePdfAttachment(args) {
|
|
|
387
400
|
if (row.kind !== "mail")
|
|
388
401
|
return attachmentNeedsMailTemplate();
|
|
389
402
|
const linked = parseLinkedResourceEntries(row.linkedResources);
|
|
403
|
+
if (isImageKey(linked, key))
|
|
404
|
+
return keyInUse();
|
|
390
405
|
if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
|
|
391
406
|
return attachmentLimitFailure();
|
|
392
407
|
}
|
|
@@ -13,10 +13,5 @@ export declare function isServedByCollectionOnly(policy: TemplateDeliveryPolicy,
|
|
|
13
13
|
export declare function isRowServed(policy: TemplateDeliveryPolicy, row: DeliveryTarget & {
|
|
14
14
|
readonly tenantId: string;
|
|
15
15
|
}): boolean;
|
|
16
|
-
export declare function isRowServedTo(policy: TemplateDeliveryPolicy, user: {
|
|
17
|
-
readonly roles: readonly string[];
|
|
18
|
-
}, row: DeliveryTarget & {
|
|
19
|
-
readonly tenantId: string;
|
|
20
|
-
}): boolean;
|
|
21
16
|
export declare function deliveryPolicyOf(registry: Registry): TemplateDeliveryPolicy;
|
|
22
17
|
export {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isPlainObject, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
-
import { isSystemAdmin } from "./template-draft.js";
|
|
3
2
|
import { SYSTEM_TENANT_ID } from "./web/constants.js";
|
|
4
3
|
import { TEMPLATE_RESOLVER_FEATURE } from "./web/qualified-names.js";
|
|
5
4
|
const NOTHING_UNLOCKED = { tenantEditable: [], collectionKinds: [] };
|
|
@@ -16,11 +15,6 @@ export function isServedByCollectionOnly(policy, target) {
|
|
|
16
15
|
export function isRowServed(policy, row) {
|
|
17
16
|
return row.tenantId === SYSTEM_TENANT_ID || isTenantOverrideServed(policy, row);
|
|
18
17
|
}
|
|
19
|
-
// A SystemAdmin sees every stored row, so a locked override can still be reset; everyone else
|
|
20
|
-
// sees only what is delivered, which keeps a locked override out of the template-resolver queries for a TenantAdmin (the audit log keeps the history).
|
|
21
|
-
export function isRowServedTo(policy, user, row) {
|
|
22
|
-
return isSystemAdmin(user) || isRowServed(policy, row);
|
|
23
|
-
}
|
|
24
18
|
function isDeliveryTarget(value) {
|
|
25
19
|
return (typeof propertyOf(value, "kind") === "string" && typeof propertyOf(value, "slug") === "string");
|
|
26
20
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { HEX_COLOR_PATTERN, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "./web/constants.js";
|
|
3
3
|
import { MAIL_TONES } from "./web/mail-constants.js";
|
|
4
|
+
import { MAIL_LAYOUT_ACCENT_LINES, MAIL_LAYOUT_BUTTON_STYLES, MAIL_LAYOUT_FONTS, MAIL_LAYOUT_WIDTHS, } from "./web/mail-layout-constants.js";
|
|
4
5
|
// One or more kebab segments joined by ":" — notification templates are seeded
|
|
5
6
|
// under their qualified name `feature:notify:name`.
|
|
6
7
|
export const templateSlugSchema = z
|
|
@@ -66,12 +67,14 @@ const pdfMetaSchema = z
|
|
|
66
67
|
.optional(),
|
|
67
68
|
})
|
|
68
69
|
.strict();
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const MAIL_LAYOUT_WIDTH_LITERALS = [
|
|
71
|
+
z.literal(MAIL_LAYOUT_WIDTHS[0]),
|
|
72
|
+
z.literal(MAIL_LAYOUT_WIDTHS[1]),
|
|
73
|
+
z.literal(MAIL_LAYOUT_WIDTHS[2]),
|
|
74
|
+
];
|
|
72
75
|
const mailLayoutMetaSchema = z
|
|
73
76
|
.object({
|
|
74
|
-
width: z.union(
|
|
77
|
+
width: z.union(MAIL_LAYOUT_WIDTH_LITERALS).optional(),
|
|
75
78
|
font: z.enum(MAIL_LAYOUT_FONTS).optional(),
|
|
76
79
|
outerBackground: z.string().regex(HEX_COLOR_PATTERN, "must be a #rrggbb color").optional(),
|
|
77
80
|
accentLine: z.enum(MAIL_LAYOUT_ACCENT_LINES).optional(),
|
|
@@ -8,7 +8,7 @@ import * as z from "zod";
|
|
|
8
8
|
import { createEmailAttachmentReader, createTemplateMailRenderer } from "../delivery/index.js";
|
|
9
9
|
import { fileContextForTenant } from "../file-foundation/index.js";
|
|
10
10
|
import { collectRendererPlugins, createRendererFoundationApi, RendererError, } from "../renderer-foundation/index.js";
|
|
11
|
-
import { createTemplateResolverApi, TemplateNotFoundError } from "./api.js";
|
|
11
|
+
import { createTemplateResolverApi, isRowDeliveredTo, TemplateNotFoundError, } from "./api.js";
|
|
12
12
|
import { collectDocumentResourceKeys } from "./compile-document-body.js";
|
|
13
13
|
import { parseStoredJson } from "./handlers/template-gates.js";
|
|
14
14
|
import { validateTemplateDraft } from "./handlers/validate.query.js";
|
|
@@ -16,7 +16,7 @@ import { readLayoutRegions, sanitizeLayoutHtml, } from "./layout-regions.js";
|
|
|
16
16
|
import { parseLinkedResourceEntries } from "./linked-resources.js";
|
|
17
17
|
import { buildDisplayHtml } from "./preview-display-html.js";
|
|
18
18
|
import { templateResourcesTable } from "./table.js";
|
|
19
|
-
import { deliveryPolicyOf
|
|
19
|
+
import { deliveryPolicyOf } from "./template-delivery-policy.js";
|
|
20
20
|
import { isMailTemplateMeta, parseTemplateMeta, readPdfTemplateMeta, } from "./template-meta.js";
|
|
21
21
|
import { DEFAULT_MAIL_LAYOUT_SLUG, DEFAULT_PDF_LAYOUT_SLUG, MAIL_PREVIEW_MAX_BYTES, SYSTEM_TENANT_ID, } from "./web/constants.js";
|
|
22
22
|
import { TemplateResolverErrors } from "./web/qualified-names.js";
|
|
@@ -149,7 +149,8 @@ export function estimateMailBytes(args) {
|
|
|
149
149
|
async function loadEditedRowResources(ctx, user, input) {
|
|
150
150
|
const rows = await selectMany(ctx.db, templateResourcesTable, { tenantId: user.tenantId, slug: input.slug, kind: "mail", locale: input.locale }, { limit: 1 });
|
|
151
151
|
const row = rows[0];
|
|
152
|
-
if (row !== undefined &&
|
|
152
|
+
if (row !== undefined &&
|
|
153
|
+
!(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), user, row))) {
|
|
153
154
|
return undefined;
|
|
154
155
|
}
|
|
155
156
|
return row === undefined ? undefined : parseLinkedResourceEntries(row.linkedResources);
|
|
@@ -10,7 +10,7 @@ export declare const UPSERT_KINDS: readonly ["mail", "document-pdf", "image-snap
|
|
|
10
10
|
export type UpsertKind = (typeof UPSERT_KINDS)[number];
|
|
11
11
|
export declare const TEXT_BLOCK_KIND: "text-block";
|
|
12
12
|
export declare const COLLECTION_REFUSED_KINDS: readonly ["mail", "mail-layout", "pdf-layout"];
|
|
13
|
-
export declare function isCollectionRefusedKind(kind:
|
|
13
|
+
export declare function isCollectionRefusedKind(kind: unknown): kind is (typeof COLLECTION_REFUSED_KINDS)[number];
|
|
14
14
|
export declare const CONTENT_FORMATS: readonly ["markdown", "mjml", "html", "plain"];
|
|
15
15
|
export type ContentFormat = (typeof CONTENT_FORMATS)[number];
|
|
16
16
|
export declare const TEMPLATE_SCOPES: readonly ["system", "tenant"];
|
|
@@ -18,8 +18,8 @@ export type TemplateScope = (typeof TEMPLATE_SCOPES)[number];
|
|
|
18
18
|
export declare const TEMPLATE_STATUSES: readonly ["draft", "active", "archived"];
|
|
19
19
|
export type TemplateStatus = (typeof TEMPLATE_STATUSES)[number];
|
|
20
20
|
export declare const ACTIVE_TEMPLATE_STATUS = "active";
|
|
21
|
+
export { HEX_COLOR_PATTERN } from "./mail-layout-constants.ts";
|
|
21
22
|
export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, type PdfOrientation, type PdfPageFormat, } from "./pdf-constants.ts";
|
|
22
|
-
export declare const HEX_COLOR_PATTERN: RegExp;
|
|
23
23
|
export declare const DEFAULT_MAIL_LAYOUT_SLUG = "default";
|
|
24
24
|
export declare const DEFAULT_PDF_LAYOUT_SLUG = "default";
|
|
25
25
|
export declare const MAIL_PREVIEW_MAX_BYTES: number;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// RenderKind identifies the consumer class of a template. Plugin renderers in
|
|
5
5
|
// `renderer-foundation` match on kind; the resolver here is kind-agnostic: it
|
|
6
6
|
// only loads, and the content format (markdown/mjml/html) decides who renders it.
|
|
7
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
7
8
|
export const RENDER_KINDS = ["mail", "document-pdf", "image-snapshot"];
|
|
8
9
|
// Kinds whose html body is a rich document: the server rebuilds it through the
|
|
9
10
|
// document schema on every write (see document-sanitizer.ts).
|
|
@@ -44,15 +45,14 @@ export const COLLECTION_REFUSED_KINDS = [
|
|
|
44
45
|
"pdf-layout",
|
|
45
46
|
];
|
|
46
47
|
export function isCollectionRefusedKind(kind) {
|
|
47
|
-
return COLLECTION_REFUSED_KINDS
|
|
48
|
+
return isOneOf(COLLECTION_REFUSED_KINDS, kind);
|
|
48
49
|
}
|
|
49
50
|
export const CONTENT_FORMATS = ["markdown", "mjml", "html", "plain"];
|
|
50
51
|
export const TEMPLATE_SCOPES = ["system", "tenant"];
|
|
51
52
|
export const TEMPLATE_STATUSES = ["draft", "active", "archived"];
|
|
52
53
|
export const ACTIVE_TEMPLATE_STATUS = "active";
|
|
54
|
+
export { HEX_COLOR_PATTERN } from "./mail-layout-constants.js";
|
|
53
55
|
export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "./pdf-constants.js";
|
|
54
|
-
// Colors end up inside MJML attributes, so only a strict hex is ever written there.
|
|
55
|
-
export const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
56
56
|
export const DEFAULT_MAIL_LAYOUT_SLUG = "default";
|
|
57
57
|
export const DEFAULT_PDF_LAYOUT_SLUG = "default";
|
|
58
58
|
// The mail transport enforces no size limit of its own; 10 MiB is a common provider ceiling.
|
|
@@ -2,6 +2,7 @@ export { textBlocksClient } from "./client-plugin.tsx";
|
|
|
2
2
|
export { DocumentContentEditor } from "./document-editor.tsx";
|
|
3
3
|
export { type DocumentEditorChrome, type DocumentEditorHandle, type DocumentEditorProps, type DocumentResourcePreview, type DocumentToolbarBlock, MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS, } from "./document-toolbar-blocks.ts";
|
|
4
4
|
export { MAIL_TONES, type MailTone } from "./mail-constants.ts";
|
|
5
|
+
export { HEX_COLOR_PATTERN, MAIL_LAYOUT_ACCENT_LINES, MAIL_LAYOUT_BUTTON_STYLES, MAIL_LAYOUT_FONTS, MAIL_LAYOUT_WIDTHS, } from "./mail-layout-constants.ts";
|
|
5
6
|
export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "./pdf-constants.ts";
|
|
6
7
|
export { PlaceholderLineEditor, type PlaceholderLineEditorProps, } from "./placeholder-line-editor-lazy.tsx";
|
|
7
8
|
export { TemplateResolverErrors, TemplateResolverHandlers, TemplateResolverQueries, } from "./qualified-names.ts";
|
|
@@ -7,6 +7,7 @@ export { textBlocksClient } from "./client-plugin.js";
|
|
|
7
7
|
export { DocumentContentEditor } from "./document-editor.js";
|
|
8
8
|
export { MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS, } from "./document-toolbar-blocks.js";
|
|
9
9
|
export { MAIL_TONES } from "./mail-constants.js";
|
|
10
|
+
export { HEX_COLOR_PATTERN, MAIL_LAYOUT_ACCENT_LINES, MAIL_LAYOUT_BUTTON_STYLES, MAIL_LAYOUT_FONTS, MAIL_LAYOUT_WIDTHS, } from "./mail-layout-constants.js";
|
|
10
11
|
export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "./pdf-constants.js";
|
|
11
12
|
export { PlaceholderLineEditor, } from "./placeholder-line-editor-lazy.js";
|
|
12
13
|
export { TemplateResolverErrors, TemplateResolverHandlers, TemplateResolverQueries, } from "./qualified-names.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const MAIL_LAYOUT_WIDTHS: readonly [560, 600, 640];
|
|
2
|
+
export declare const MAIL_LAYOUT_FONTS: readonly ["sans", "serif"];
|
|
3
|
+
export declare const MAIL_LAYOUT_ACCENT_LINES: readonly ["branding", "none"];
|
|
4
|
+
export declare const MAIL_LAYOUT_BUTTON_STYLES: readonly ["filled-rounded", "filled-square", "outline"];
|
|
5
|
+
export declare const HEX_COLOR_PATTERN: RegExp;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// No imports: web/ re-exports these, and template-meta.ts pulls in zod and the framework engine.
|
|
2
|
+
export const MAIL_LAYOUT_WIDTHS = [560, 600, 640];
|
|
3
|
+
export const MAIL_LAYOUT_FONTS = ["sans", "serif"];
|
|
4
|
+
export const MAIL_LAYOUT_ACCENT_LINES = ["branding", "none"];
|
|
5
|
+
export const MAIL_LAYOUT_BUTTON_STYLES = ["filled-rounded", "filled-square", "outline"];
|
|
6
|
+
// Colors end up inside MJML attributes, so only a strict hex is ever written there.
|
|
7
|
+
export const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
@@ -2,6 +2,7 @@ import * as z from "zod";
|
|
|
2
2
|
export declare const memberDirectoryQuery: import("@cosmicdrift/kumiko-framework/engine").PagedQueryHandlerDefinition<"memberDirectory", z.ZodObject<{
|
|
3
3
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
4
4
|
search: z.ZodOptional<z.ZodString>;
|
|
5
|
+
ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
5
6
|
}, z.core.$strip>, {
|
|
6
7
|
id: string;
|
|
7
8
|
label: string;
|
|
@@ -40,6 +40,8 @@ export const memberDirectoryQuery = definePagedQueryHandler({
|
|
|
40
40
|
limit: z.number().int().min(1).max(MAX_LIST_LIMIT).default(MAX_LIST_LIMIT),
|
|
41
41
|
// Sent by the reference combobox while the user types.
|
|
42
42
|
search: z.string().trim().min(1).optional(),
|
|
43
|
+
// Resolves exactly these users; callers that know their ids must not depend on the cap.
|
|
44
|
+
ids: z.array(z.string().min(1)).max(MAX_LIST_LIMIT).optional(),
|
|
43
45
|
}),
|
|
44
46
|
access: { roles: access.admin },
|
|
45
47
|
description: "Lists users as id/label pairs for resolving user references to display names: the caller's own tenant members for an admin, every user for a SystemAdmin; carries no email or role data.",
|
|
@@ -50,21 +52,26 @@ export const memberDirectoryQuery = definePagedQueryHandler({
|
|
|
50
52
|
message: "tenant:query:member-directory requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
|
-
const { limit, search } = query.payload;
|
|
55
|
+
const { limit, search, ids } = query.payload;
|
|
56
|
+
if (ids?.length === 0)
|
|
57
|
+
return { rows: [], nextCursor: null };
|
|
54
58
|
// Without a search the first `limit` rows are enough; with one, every
|
|
55
59
|
// candidate must be decrypted before the limit can apply.
|
|
56
60
|
const scanLimit = search === undefined ? limit : SEARCH_SCAN_CAP;
|
|
57
61
|
let users;
|
|
58
62
|
if (isSystemAdmin(query.user)) {
|
|
59
63
|
const db = ctx.systemDb.acknowledgeCrossTenant("SystemAdmin reference labels span every tenant, as user:query:user:list did");
|
|
60
|
-
users = await selectMany(db, userTable, undefined, {
|
|
64
|
+
users = await selectMany(db, userTable, ids === undefined ? undefined : { id: ids }, {
|
|
61
65
|
limit: scanLimit,
|
|
62
66
|
orderBy: { col: "id", direction: "asc" },
|
|
63
67
|
});
|
|
64
68
|
}
|
|
65
69
|
else {
|
|
66
70
|
const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
|
|
67
|
-
const
|
|
71
|
+
const requestedIds = ids === undefined ? undefined : new Set(ids);
|
|
72
|
+
const userIds = (await loadMemberUserIds(db, query.user.tenantId))
|
|
73
|
+
.filter((id) => requestedIds === undefined || requestedIds.has(id))
|
|
74
|
+
.slice(0, scanLimit);
|
|
68
75
|
users =
|
|
69
76
|
userIds.length > 0
|
|
70
77
|
? await selectMany(db, userTable, { id: [...userIds] }, {
|
|
@@ -2,6 +2,7 @@ import * as z from "zod";
|
|
|
2
2
|
export declare const tenantDirectoryQuery: import("@cosmicdrift/kumiko-framework/engine").PagedQueryHandlerDefinition<"tenantDirectory", z.ZodObject<{
|
|
3
3
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
4
4
|
search: z.ZodOptional<z.ZodString>;
|
|
5
|
+
ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
5
6
|
}, z.core.$strip>, {
|
|
6
7
|
id: string;
|
|
7
8
|
label: string;
|
|
@@ -21,9 +21,11 @@ export const tenantDirectoryQuery = definePagedQueryHandler({
|
|
|
21
21
|
limit: z.number().int().min(1).max(MAX_LIST_LIMIT).default(MAX_LIST_LIMIT),
|
|
22
22
|
// Sent by the reference combobox while the user types.
|
|
23
23
|
search: z.string().trim().min(1).optional(),
|
|
24
|
+
// Resolves exactly these tenants; callers that know their ids must not depend on the cap.
|
|
25
|
+
ids: z.array(z.string().min(1)).max(MAX_LIST_LIMIT).optional(),
|
|
24
26
|
}),
|
|
25
27
|
access: { roles: access.admin },
|
|
26
|
-
description: "Lists tenants as id/label pairs for resolving tenant references to display names: the caller's own tenant for an admin, every tenant for a SystemAdmin.",
|
|
28
|
+
description: "Lists tenants as id/label pairs for resolving tenant references to display names: the caller's own tenant for an admin, every tenant for a SystemAdmin. A search returns the first `limit` matches in tenant-key order (sorted by label afterwards), not the alphabetically best matches.",
|
|
27
29
|
agent: { expose: false },
|
|
28
30
|
handler: async (query, ctx) => {
|
|
29
31
|
if (!ctx.systemDb) {
|
|
@@ -31,12 +33,14 @@ export const tenantDirectoryQuery = definePagedQueryHandler({
|
|
|
31
33
|
message: "tenant:query:tenant-directory requires ctx.systemDb — is r.systemScope() still set on the tenant feature?",
|
|
32
34
|
});
|
|
33
35
|
}
|
|
34
|
-
const { limit, search } = query.payload;
|
|
36
|
+
const { limit, search, ids } = query.payload;
|
|
37
|
+
if (ids?.length === 0)
|
|
38
|
+
return { rows: [], nextCursor: null };
|
|
35
39
|
const scanLimit = search === undefined ? limit : SEARCH_SCAN_CAP;
|
|
36
40
|
let tenants;
|
|
37
41
|
if (isSystemAdmin(query.user)) {
|
|
38
42
|
const db = ctx.systemDb.acknowledgeCrossTenant("SystemAdmin reference labels span every tenant, as tenant:query:tenant:list did");
|
|
39
|
-
tenants = await selectMany(db, tenantTable, undefined, {
|
|
43
|
+
tenants = await selectMany(db, tenantTable, ids === undefined ? undefined : { id: ids }, {
|
|
40
44
|
limit: scanLimit,
|
|
41
45
|
orderBy: [
|
|
42
46
|
{ col: "key", direction: "asc" },
|
|
@@ -47,7 +51,7 @@ export const tenantDirectoryQuery = definePagedQueryHandler({
|
|
|
47
51
|
else {
|
|
48
52
|
const db = ctx.systemDb.assertTenantMatch(query.user.tenantId);
|
|
49
53
|
const row = await fetchOne(db, tenantTable, { id: query.user.tenantId });
|
|
50
|
-
tenants = row ? [row] : [];
|
|
54
|
+
tenants = row && (ids === undefined || ids.includes(query.user.tenantId)) ? [row] : [];
|
|
51
55
|
}
|
|
52
56
|
// name is ciphertext at rest, so the SQL page is keyed by the plaintext
|
|
53
57
|
// `key`; label order and search happen in memory after decrypting.
|
|
@@ -27,3 +27,4 @@ export declare const TenantLifecycleHandlers: {
|
|
|
27
27
|
export declare const TENANT_DESTRUCTION_STAGES: readonly ["external-resources", "search-indices", "cache", "app-data", "subject-keys", "files", "infra-resources", "tenant-row"];
|
|
28
28
|
export type TenantDestructionStageName = (typeof TENANT_DESTRUCTION_STAGES)[number];
|
|
29
29
|
export declare const DESTRUCTION_STAGE_TICK_BUDGET_MS = 30000;
|
|
30
|
+
export declare const DESTRUCTION_STAGE_MIN_SLICE_MS = 2000;
|
|
@@ -38,3 +38,7 @@ export const TENANT_DESTRUCTION_STAGES = [
|
|
|
38
38
|
// The sweep cron fires every minute with concurrency "skip", so a tick must
|
|
39
39
|
// finish well inside that window or the next one is skipped.
|
|
40
40
|
export const DESTRUCTION_STAGE_TICK_BUDGET_MS = 30_000;
|
|
41
|
+
// Lower bound of a tenant's share of the tick budget. Below it a chunking hook
|
|
42
|
+
// without a guaranteed first batch processes nothing, and a no-progress stage
|
|
43
|
+
// counts as a failed attempt, which would abandon the pipeline under load.
|
|
44
|
+
export const DESTRUCTION_STAGE_MIN_SLICE_MS = 2_000;
|
|
@@ -3,7 +3,7 @@ import { createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
3
3
|
import { append, loadAggregate, VersionConflictError, } from "@cosmicdrift/kumiko-framework/event-store";
|
|
4
4
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
5
5
|
import { tenantEntity, tenantTable } from "../tenant/index.js";
|
|
6
|
-
import { DESTRUCTION_STAGE_TICK_BUDGET_MS, TENANT_AGGREGATE_TYPE, TENANT_DESTRUCTION_COMPLETED_EVENT_QN, TENANT_DESTRUCTION_FAILED_EVENT_QN, TENANT_DESTRUCTION_STAGE_ABANDONED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_FAILED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_PROGRESSED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_STARTED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_SUCCEEDED_EVENT_SHORT, TENANT_DESTRUCTION_STARTED_EVENT_SHORT, } from "./constants.js";
|
|
6
|
+
import { DESTRUCTION_STAGE_MIN_SLICE_MS, DESTRUCTION_STAGE_TICK_BUDGET_MS, TENANT_AGGREGATE_TYPE, TENANT_DESTRUCTION_COMPLETED_EVENT_QN, TENANT_DESTRUCTION_FAILED_EVENT_QN, TENANT_DESTRUCTION_STAGE_ABANDONED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_FAILED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_PROGRESSED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_STARTED_EVENT_SHORT, TENANT_DESTRUCTION_STAGE_SUCCEEDED_EVENT_SHORT, TENANT_DESTRUCTION_STARTED_EVENT_SHORT, } from "./constants.js";
|
|
7
7
|
import { invalidateTenantLifecycleGate, TEARDOWN_GATE_SETTLE_MS } from "./lifecycle-gate.js";
|
|
8
8
|
import { isDestructionPipelineComplete, pickNextStage, } from "./stages.js";
|
|
9
9
|
const tenantCrud = createEventStoreExecutor(tenantTable, tenantEntity, { entityName: "tenant" });
|
|
@@ -251,7 +251,7 @@ export async function runTenantDestructionSweep(args) {
|
|
|
251
251
|
// window and starve the ones behind it. Each slice starts when its tenant
|
|
252
252
|
// starts: anchoring to a shared tick deadline would hand tenants after an
|
|
253
253
|
// overrun a zero slice, and a no-progress stage counts as a failed attempt.
|
|
254
|
-
const sliceMs = DESTRUCTION_STAGE_TICK_BUDGET_MS / Math.max(1, destroying.length);
|
|
254
|
+
const sliceMs = Math.max(DESTRUCTION_STAGE_MIN_SLICE_MS, DESTRUCTION_STAGE_TICK_BUDGET_MS / Math.max(1, destroying.length));
|
|
255
255
|
for (const row of destroying) {
|
|
256
256
|
const tenantDeadlineAt = Date.now() + sliceMs;
|
|
257
257
|
try {
|
|
@@ -16,9 +16,8 @@ export declare function createSetTenantTierWrite(opts?: SetTenantTierOptions): i
|
|
|
16
16
|
tenantId: z.ZodString;
|
|
17
17
|
tier: z.ZodString;
|
|
18
18
|
source: z.ZodDefault<z.ZodEnum<{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
readonly default: "default";
|
|
19
|
+
billing: "billing";
|
|
20
|
+
manual: "manual";
|
|
22
21
|
}>>;
|
|
23
22
|
}, z.core.$strip>, {
|
|
24
23
|
tenantId: string;
|
|
@@ -38,7 +38,10 @@ export function createSetTenantTierWrite(opts = {}) {
|
|
|
38
38
|
.min(1)
|
|
39
39
|
.max(50)
|
|
40
40
|
.refine((t) => !opts.validTiers || opts.validTiers.has(t), { message: "unknown tier" }),
|
|
41
|
-
|
|
41
|
+
// `default` is reserved for the signup default tier; the billing sync treats it like `billing`.
|
|
42
|
+
source: z
|
|
43
|
+
.enum([TierAssignmentSources.manual, TierAssignmentSources.billing])
|
|
44
|
+
.default(TierAssignmentSources.manual),
|
|
42
45
|
}),
|
|
43
46
|
access: { roles: ["SystemAdmin"] },
|
|
44
47
|
agent: { risk: "high" },
|