@cosmicdrift/kumiko-bundled-features 0.364.0 → 0.365.1
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/audit/feature.js +1 -1
- package/dist/audit/run-escape-hatch-retention.d.ts +1 -0
- package/dist/audit/run-escape-hatch-retention.js +39 -12
- package/dist/auth-email-password/bootstrap.d.ts +1 -3
- package/dist/auth-email-password/feature.js +2 -0
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -3
- package/dist/auth-email-password/handlers/invite-create.write.d.ts +2 -3
- package/dist/auth-email-password/handlers/invite-create.write.js +4 -1
- package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.js +1 -1
- package/dist/auth-email-password/magic-link-mail.js +6 -3
- package/dist/auth-email-password/mail-i18n.d.ts +3 -0
- package/dist/auth-email-password/mail-i18n.js +10 -0
- package/dist/billing-foundation/consumer-protection/termination-record.js +8 -1
- package/dist/billing-foundation/i18n.js +6 -0
- package/dist/billing-foundation/subscription-tier-sync.js +8 -2
- package/dist/cap-counter/book-cap-usage.js +8 -0
- package/dist/cap-counter/cap-reservation.js +7 -3
- package/dist/cap-counter/handlers/increment-rolling.write.d.ts +1 -0
- package/dist/cap-counter/handlers/increment-rolling.write.js +2 -0
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +3 -1
- package/dist/cap-overview/handlers/tenant-options.query.js +3 -1
- package/dist/channel-discord/discord-channel.js +3 -2
- package/dist/channel-email/email-channel.js +24 -19
- package/dist/channel-email/smtp-transport.js +6 -1
- package/dist/channel-slack/slack-channel.js +3 -2
- package/dist/channel-teams/teams-channel.js +3 -2
- package/dist/crypto-shredding/handlers/forget-subject.write.js +2 -2
- package/dist/custom-fields/db/queries/retention.d.ts +1 -1
- package/dist/custom-fields/db/queries/retention.js +2 -1
- package/dist/data-retention/run-retention-cleanup.js +4 -5
- package/dist/delivery/attachment-ref.js +16 -1
- package/dist/delivery/delivery-service.js +71 -82
- package/dist/feature-toggles/toggle-runtime.d.ts +5 -3
- package/dist/feature-toggles/toggle-runtime.js +25 -12
- package/dist/files-tenant-data/index.d.ts +1 -0
- package/dist/files-tenant-data/index.js +2 -1
- package/dist/form-draft/db/queries/owned-file-refs.d.ts +1 -1
- package/dist/form-draft/db/queries/owned-file-refs.js +9 -2
- package/dist/form-draft/handlers/discard.write.js +1 -1
- package/dist/jobs/job-run-logger.js +5 -2
- package/dist/metrics/filters.d.ts +16 -1
- package/dist/metrics/filters.js +27 -11
- package/dist/personal-access-tokens/revoke-for-user.js +3 -7
- package/dist/secrets/handlers/delete.write.js +3 -1
- package/dist/secrets/write-gate.d.ts +3 -1
- package/dist/secrets/write-gate.js +6 -3
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/resource-image-formats.d.ts +23 -0
- package/dist/shared/resource-image-formats.js +21 -0
- package/dist/shared/sharp-input-limits.js +7 -8
- package/dist/step-dispatcher/constants.d.ts +10 -0
- package/dist/step-dispatcher/constants.js +17 -0
- package/dist/step-dispatcher/deliver-dispatch-request.d.ts +14 -0
- package/dist/step-dispatcher/deliver-dispatch-request.js +163 -0
- package/dist/step-dispatcher/feature.js +47 -100
- package/dist/step-dispatcher/webhook-runner.d.ts +7 -4
- package/dist/step-dispatcher/webhook-runner.js +26 -11
- package/dist/template-resolver/compile-document-body.d.ts +3 -0
- package/dist/template-resolver/compile-document-body.js +63 -12
- package/dist/template-resolver/compile-steps-block.d.ts +10 -0
- package/dist/template-resolver/compile-steps-block.js +45 -0
- package/dist/template-resolver/constants.d.ts +4 -1
- package/dist/template-resolver/constants.js +10 -1
- package/dist/template-resolver/document-sanitizer.js +3 -0
- package/dist/template-resolver/document-schema.d.ts +2 -1
- package/dist/template-resolver/document-schema.js +6 -1
- package/dist/template-resolver/document-steps.d.ts +17 -0
- package/dist/template-resolver/document-steps.js +93 -0
- package/dist/template-resolver/feature.js +9 -0
- package/dist/template-resolver/handlers/bundle-import.d.ts +7 -1
- package/dist/template-resolver/handlers/bundle-import.js +63 -38
- package/dist/template-resolver/handlers/collection-set.write.js +4 -3
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +1 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +83 -55
- package/dist/template-resolver/handlers/diff-to-system.query.js +1 -0
- package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +1 -1
- package/dist/template-resolver/handlers/import-bundle-system.write.js +16 -5
- package/dist/template-resolver/handlers/import-bundle.write.d.ts +1 -1
- package/dist/template-resolver/handlers/import-bundle.write.js +11 -1
- package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
- package/dist/template-resolver/handlers/overview.query.js +9 -1
- package/dist/template-resolver/handlers/resource-upload.write.d.ts +2 -2
- package/dist/template-resolver/handlers/resource-upload.write.js +35 -5
- package/dist/template-resolver/handlers/validate.query.js +4 -1
- package/dist/template-resolver/index.d.ts +3 -3
- package/dist/template-resolver/index.js +3 -3
- package/dist/template-resolver/layout-appearance.d.ts +1 -1
- package/dist/template-resolver/layout-appearance.js +19 -9
- package/dist/template-resolver/mail-constants.d.ts +2 -0
- package/dist/template-resolver/mail-constants.js +3 -0
- package/dist/template-resolver/resource-image.d.ts +2 -9
- package/dist/template-resolver/resource-image.js +2 -11
- package/dist/template-resolver/seeding.js +2 -0
- package/dist/template-resolver/store-template-resource.d.ts +53 -9
- package/dist/template-resolver/store-template-resource.js +250 -97
- package/dist/template-resolver/template-bundle.js +4 -0
- package/dist/template-resolver/template-draft.d.ts +3 -0
- package/dist/template-resolver/template-draft.js +1 -1
- package/dist/template-resolver/template-meta.d.ts +5 -1
- package/dist/template-resolver/template-meta.js +7 -0
- package/dist/template-resolver/template-preview.js +1 -2
- package/dist/template-resolver/template-resources.d.ts +0 -5
- package/dist/template-resolver/template-resources.js +2 -34
- package/dist/template-resolver/template-validation.js +12 -9
- package/dist/template-resolver/web/document-editor-extensions.d.ts +1 -0
- package/dist/template-resolver/web/document-editor-extensions.js +26 -1
- package/dist/template-resolver/web/document-editor.js +5 -1
- package/dist/template-resolver/web/document-tiptap-editor.js +30 -7
- package/dist/template-resolver/web/document-toolbar-blocks.d.ts +5 -5
- package/dist/template-resolver/web/document-toolbar-blocks.js +2 -0
- package/dist/template-resolver/web/i18n.js +5 -0
- package/dist/template-resolver/web/index.d.ts +1 -0
- package/dist/template-resolver/web/index.js +1 -0
- package/dist/template-resolver/web/placeholder-line-editor.d.ts +4 -2
- package/dist/template-resolver/web/placeholder-line-editor.js +8 -6
- package/dist/tenant/handlers/update-member-roles.write.js +3 -6
- package/dist/tenant/seeding.js +23 -24
- package/dist/tenant-handover/move-entity-graph.js +14 -4
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +1 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +8 -1
- package/dist/tenant-lifecycle/stages.js +7 -3
- package/dist/tier-engine/constants.d.ts +2 -0
- package/dist/tier-engine/constants.js +4 -0
- package/dist/tier-engine/entity.d.ts +15 -14
- package/dist/tier-engine/entity.js +4 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +5 -0
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +7 -4
- package/dist/tier-engine/index.d.ts +1 -1
- package/dist/tier-engine/index.js +1 -1
- package/dist/tier-engine/tier-resolver.d.ts +13 -13
- package/dist/user/constants.d.ts +1 -0
- package/dist/user/invited-global-roles.d.ts +2 -4
- package/dist/user-data-rights/email-templates.js +0 -1
- package/dist/user-data-rights/i18n.js +5 -0
- package/dist/user-data-rights/lib/update-user-lifecycle.d.ts +2 -0
- package/dist/user-data-rights/lib/update-user-lifecycle.js +6 -2
- package/dist/user-data-rights/run-forget-cleanup.js +2 -2
- package/dist/waitlist/feature.js +1 -1
- package/dist/waitlist/handlers/invite.write.js +21 -3
- package/dist/waitlist/handlers/reject.write.js +3 -16
- package/dist/waitlist/handlers/submit.write.js +29 -7
- package/dist/waitlist/i18n.js +9 -0
- package/dist/waitlist/lib.d.ts +2 -1
- package/dist/waitlist/lib.js +15 -0
- package/dist/workflow-runner/feature.js +1 -1
- package/package.json +10 -10
- package/src/audit/changes.json +12 -0
- package/src/auth-email-password/changes.json +18 -0
- package/src/billing-foundation/changes.json +12 -0
- package/src/cap-counter/changes.json +13 -0
- package/src/cap-overview/changes.json +6 -0
- package/src/channel-email/changes.json +18 -0
- package/src/crypto-shredding/changes.json +7 -0
- package/src/delivery/changes.json +12 -0
- package/src/document-ingest-foundation/changes.json +6 -0
- package/src/feature-toggles/changes.json +7 -0
- package/src/jobs/changes.json +12 -0
- package/src/metrics/changes.json +6 -0
- package/src/personal-access-tokens/changes.json +6 -0
- package/src/secrets/changes.json +6 -0
- package/src/step-dispatcher/changes.json +8 -1
- package/src/template-resolver/changes.json +81 -0
- package/src/tenant/changes.json +6 -0
- package/src/tenant-lifecycle/changes.json +6 -0
- package/src/tier-engine/changes.json +12 -0
- package/src/user-data-rights/changes.json +6 -0
- package/src/waitlist/changes.json +18 -0
package/dist/audit/feature.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { aggregateWhere, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
|
+
import { aggregateWhere, runnerCanBeginTransaction, selectMany, } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { COMPLIANCE_PROFILES, subtractRetentionSpec, } from "@cosmicdrift/kumiko-framework/compliance";
|
|
3
3
|
import { parseTenantId, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
4
|
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -10,7 +10,7 @@ import { DEFAULT_ESCAPE_HATCH_RETENTION_DAYS, ESCAPE_HATCH_RETENTION_DAYS_KEY, }
|
|
|
10
10
|
import { ESCAPE_HATCH_USE_AGGREGATE_TYPE, escapeHatchUsedSchema, } from "./escape-hatch-audit-sink.js";
|
|
11
11
|
// pruneEvents opens its own transaction, so a runner that cannot begin one (a tx handle) is rejected instead of nested.
|
|
12
12
|
export function isDbConnection(runner) {
|
|
13
|
-
return
|
|
13
|
+
return runnerCanBeginTransaction(runner);
|
|
14
14
|
}
|
|
15
15
|
async function resolveRetentionDays(args) {
|
|
16
16
|
if (!args.configResolver)
|
|
@@ -19,18 +19,29 @@ async function resolveRetentionDays(args) {
|
|
|
19
19
|
const raw = await config(ESCAPE_HATCH_RETENTION_DAYS_KEY);
|
|
20
20
|
return typeof raw === "number" && raw >= 1 ? raw : DEFAULT_ESCAPE_HATCH_RETENTION_DAYS;
|
|
21
21
|
}
|
|
22
|
+
// Only an operator-set value counts: the key's declared default must not override the tenant profiles.
|
|
23
|
+
async function resolveExplicitRetentionDays(args) {
|
|
24
|
+
const keyDef = args.registry.getConfigKey(ESCAPE_HATCH_RETENTION_DAYS_KEY);
|
|
25
|
+
if (!args.configResolver || !keyDef)
|
|
26
|
+
return undefined;
|
|
27
|
+
const { value, source } = await args.configResolver.getWithSource(ESCAPE_HATCH_RETENTION_DAYS_KEY, keyDef, SYSTEM_TENANT_ID, args.userId, args.db);
|
|
28
|
+
const isOperatorSet = source !== "default" && source !== "missing";
|
|
29
|
+
return isOperatorSet && typeof value === "number" && value >= 1 ? value : undefined;
|
|
30
|
+
}
|
|
22
31
|
// Soft dependency: audit must stay mountable without compliance-profiles, so we probe its entity.
|
|
23
32
|
const COMPLIANCE_PROFILE_ENTITY = "tenant-compliance-profile";
|
|
24
33
|
const PRUNE_CHUNK_SIZE = 1000;
|
|
34
|
+
const PRUNE_BATCH_SIZE = 5000;
|
|
25
35
|
// Overrides may only lengthen retention: the earlier cutoff (same `now`) is the longer period.
|
|
26
36
|
function earlierInstant(a, b) {
|
|
27
37
|
return Temporal.Instant.compare(a, b) <= 0 ? a : b;
|
|
28
38
|
}
|
|
29
|
-
async function resolveTenantCutoff(db, tenantId, now) {
|
|
39
|
+
async function resolveTenantCutoff(db, tenantId, now, operatorFloor) {
|
|
30
40
|
const { profile } = await resolveProfileForTenant({ db, tenantId });
|
|
31
41
|
const effective = subtractRetentionSpec(now, profile.auditLog.retention);
|
|
32
42
|
const base = subtractRetentionSpec(now, COMPLIANCE_PROFILES[profile.key].auditLog.retention);
|
|
33
|
-
|
|
43
|
+
const profileCutoff = earlierInstant(effective, base);
|
|
44
|
+
return operatorFloor ? earlierInstant(profileCutoff, operatorFloor) : profileCutoff;
|
|
34
45
|
}
|
|
35
46
|
// A cross-tenant audit (e.g. identity-switch) lives under the actor's tenant but must also
|
|
36
47
|
// satisfy the target tenant's retention. A malformed payload falls back to the storing tenant.
|
|
@@ -45,14 +56,17 @@ async function storingTenantIds(db) {
|
|
|
45
56
|
return tenantId ? [tenantId] : [];
|
|
46
57
|
});
|
|
47
58
|
}
|
|
48
|
-
async function pruneByTenantProfiles(db) {
|
|
59
|
+
async function pruneByTenantProfiles(db, operatorRetentionDays) {
|
|
49
60
|
const now = getTemporal().Now.instant();
|
|
61
|
+
const operatorFloor = operatorRetentionDays === undefined
|
|
62
|
+
? undefined
|
|
63
|
+
: now.subtract({ hours: operatorRetentionDays * 24 });
|
|
50
64
|
const cutoffByTenant = new Map();
|
|
51
65
|
const cutoffOf = async (tenantId) => {
|
|
52
66
|
const cached = cutoffByTenant.get(tenantId);
|
|
53
67
|
if (cached)
|
|
54
68
|
return cached;
|
|
55
|
-
const cutoff = await resolveTenantCutoff(db, tenantId, now);
|
|
69
|
+
const cutoff = await resolveTenantCutoff(db, tenantId, now, operatorFloor);
|
|
56
70
|
cutoffByTenant.set(tenantId, cutoff);
|
|
57
71
|
return cutoff;
|
|
58
72
|
};
|
|
@@ -87,6 +101,22 @@ async function pruneByTenantProfiles(db) {
|
|
|
87
101
|
}
|
|
88
102
|
return deletedCount;
|
|
89
103
|
}
|
|
104
|
+
async function pruneInBatches(db, olderThanDays, batchSize = PRUNE_BATCH_SIZE) {
|
|
105
|
+
const olderThan = getTemporal()
|
|
106
|
+
.Now.instant()
|
|
107
|
+
.subtract({ hours: olderThanDays * 24 });
|
|
108
|
+
let deletedCount = 0;
|
|
109
|
+
for (;;) {
|
|
110
|
+
const batch = await pruneEvents(db, {
|
|
111
|
+
aggregateTypes: [ESCAPE_HATCH_USE_AGGREGATE_TYPE],
|
|
112
|
+
olderThan,
|
|
113
|
+
batchSize,
|
|
114
|
+
});
|
|
115
|
+
deletedCount += batch.deletedCount;
|
|
116
|
+
if (batch.deletedCount < batchSize)
|
|
117
|
+
return deletedCount;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
90
120
|
export async function runEscapeHatchRetention(args) {
|
|
91
121
|
const { db } = args;
|
|
92
122
|
if (!isDbConnection(db)) {
|
|
@@ -94,11 +124,8 @@ export async function runEscapeHatchRetention(args) {
|
|
|
94
124
|
}
|
|
95
125
|
if (!args.registry.getEntity(COMPLIANCE_PROFILE_ENTITY)) {
|
|
96
126
|
const olderThanDays = await resolveRetentionDays({ ...args, db });
|
|
97
|
-
|
|
98
|
-
aggregateTypes: [ESCAPE_HATCH_USE_AGGREGATE_TYPE],
|
|
99
|
-
olderThanDays,
|
|
100
|
-
});
|
|
101
|
-
return { deletedCount };
|
|
127
|
+
return { deletedCount: await pruneInBatches(db, olderThanDays, args.batchSize) };
|
|
102
128
|
}
|
|
103
|
-
|
|
129
|
+
const operatorRetentionDays = await resolveExplicitRetentionDays({ ...args, db });
|
|
130
|
+
return { deletedCount: await pruneByTenantProfiles(db, operatorRetentionDays) };
|
|
104
131
|
}
|
|
@@ -4,8 +4,7 @@ import { type Registry, type TenantId, type WriteResult } from "@cosmicdrift/kum
|
|
|
4
4
|
import type { Dispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
5
5
|
import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
|
|
6
6
|
import type { Redis } from "ioredis";
|
|
7
|
-
import type {
|
|
8
|
-
type GlobalRole = (typeof GLOBAL_ROLE_OPTIONS)[number];
|
|
7
|
+
import type { GlobalRole } from "../user/constants.js";
|
|
9
8
|
export type BootstrapInvite = {
|
|
10
9
|
readonly email: string;
|
|
11
10
|
/** Membership role in the tenant, validated like a tenant-admin invite
|
|
@@ -80,4 +79,3 @@ export declare class BootstrapPlanError extends Error {
|
|
|
80
79
|
export declare class BootstrapWriteError extends Error {
|
|
81
80
|
}
|
|
82
81
|
export declare function bootstrapTenants(deps: BootstrapDeps, plan: BootstrapPlan): Promise<BootstrapReport>;
|
|
83
|
-
export {};
|
|
@@ -19,6 +19,7 @@ import { createSignupConfirmHandler } from "./handlers/signup-confirm.write.js";
|
|
|
19
19
|
import { createSignupRequestHandler, } from "./handlers/signup-request.write.js";
|
|
20
20
|
import { createSwitchTenantMfaGateHandler } from "./handlers/switch-tenant-mfa-gate.write.js";
|
|
21
21
|
import { createVerifyEmailHandler } from "./handlers/verify-email.write.js";
|
|
22
|
+
import { AUTH_EMAIL_PASSWORD_MAIL_I18N } from "./mail-i18n.js";
|
|
22
23
|
import { AUTH_MAIL_VARIABLES, authMailLocalizedTemplate, INVITE_MAIL_VARIABLES, sendsMagicLinkMail, } from "./mail-notifications.js";
|
|
23
24
|
/**
|
|
24
25
|
* Env-vars contract for the `auth-email-password` feature.
|
|
@@ -89,6 +90,7 @@ export function createAuthEmailPasswordFeature(opts = {}) {
|
|
|
89
90
|
if (sendsMagicLinkMail(opts))
|
|
90
91
|
r.requires("delivery");
|
|
91
92
|
r.envSchema(authEmailPasswordEnvSchema);
|
|
93
|
+
r.translations({ keys: AUTH_EMAIL_PASSWORD_MAIL_I18N });
|
|
92
94
|
// Declared unconditionally: apps also issue the activation link from their
|
|
93
95
|
// own flows (issueSignupActivation) without enabling the signup option.
|
|
94
96
|
r.notification("password-reset", {
|
|
@@ -56,9 +56,8 @@ export function createInviteAcceptWithLoginHandler(opts = {}) {
|
|
|
56
56
|
rateLimit: { per: "ip+handler", limit: 20, windowSeconds: 60 },
|
|
57
57
|
escapeHatch: {
|
|
58
58
|
grants: ["unsafeRaw"],
|
|
59
|
-
reason: "Anonymous invite-accept has no session in the invited tenant yet —
|
|
60
|
-
"
|
|
61
|
-
"Also reads the pending invitation by id (the invitee is not yet a member of the " +
|
|
59
|
+
reason: "Anonymous invite-accept has no session in the invited tenant yet — reads the pending " +
|
|
60
|
+
"invitation by id (the invitee is not yet a member of the " +
|
|
62
61
|
"invitation's tenant), adds the membership and accepts the invitation in the " +
|
|
63
62
|
"invitation's tenant, which differs from the caller's tenant, and reads the MFA " +
|
|
64
63
|
"enrollment of that tenant via the mfaStatusChecker callback.",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { type GlobalRole } from "../../user/constants.js";
|
|
3
3
|
import type { AuthMailLocale } from "../email-templates.js";
|
|
4
|
-
type GlobalRole = (typeof GLOBAL_ROLE_OPTIONS)[number];
|
|
5
4
|
export type InviteCreateData = {
|
|
6
5
|
readonly kind: "invite-created";
|
|
7
6
|
readonly invitationId: string;
|
|
@@ -40,5 +39,5 @@ export declare function createSystemInviteCreateHandler(opts: InviteCreateOption
|
|
|
40
39
|
globalRoles: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
41
40
|
SystemAdmin: "SystemAdmin";
|
|
42
41
|
}>>>;
|
|
42
|
+
locale: z.ZodOptional<z.ZodString>;
|
|
43
43
|
}, z.core.$strip>, InviteCreateData, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
44
|
-
export {};
|
|
@@ -20,6 +20,7 @@ import { generateToken } from "@cosmicdrift/kumiko-framework/api";
|
|
|
20
20
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
21
21
|
import { access, assignableAppRolesOf, defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
|
|
22
22
|
import { InternalError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
23
|
+
import { isValidLocaleTag } from "@cosmicdrift/kumiko-framework/i18n";
|
|
23
24
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
24
25
|
import * as z from "zod";
|
|
25
26
|
// kumiko-lint-ignore cross-feature-import invite-flow lebt in auth-email-password (Magic-Link-Pattern), DB-row-owner ist tenant-feature
|
|
@@ -133,7 +134,7 @@ async function issueInvitation(opts, ttlSeconds, request, inviter, ctx, refuseAc
|
|
|
133
134
|
}
|
|
134
135
|
const token = generateToken();
|
|
135
136
|
await storeInviteToken(ctx.redis, { invitationId, token, ttlSeconds });
|
|
136
|
-
const locale = resolveHandlerMailLocale(ctx, opts.locale);
|
|
137
|
+
const locale = request.locale ?? resolveHandlerMailLocale(ctx, opts.locale);
|
|
137
138
|
await dispatchMagicLinkMail(ctx.notify, {
|
|
138
139
|
handlerName: "invite-create",
|
|
139
140
|
notificationType: AuthMailNotificationTypes.invite,
|
|
@@ -188,6 +189,8 @@ const SystemInviteCreateSchema = z.object({
|
|
|
188
189
|
email: z.email(),
|
|
189
190
|
role: z.string().min(1).max(50),
|
|
190
191
|
globalRoles: z.array(z.enum(GLOBAL_ROLE_OPTIONS)).max(GLOBAL_ROLE_OPTIONS.length).default([]),
|
|
192
|
+
// Preferred over the request locale: provisioning callers know the invitee's language.
|
|
193
|
+
locale: z.string().max(35).refine(isValidLocaleTag, "invalid locale tag").optional(),
|
|
191
194
|
});
|
|
192
195
|
// System-only sibling of invite-create: the only path that can put global
|
|
193
196
|
// roles (SystemAdmin) on an invitation. No session mint path yields the
|
|
@@ -2,6 +2,7 @@ import * as z from "zod";
|
|
|
2
2
|
import { gateEnforceMfa, type LoginHandlerOptions } from "./login.write.js";
|
|
3
3
|
export type SwitchTenantMfaGateData = {
|
|
4
4
|
readonly kind: "mfa-gate-clear";
|
|
5
|
+
readonly roles: readonly string[];
|
|
5
6
|
} | NonNullable<Awaited<ReturnType<typeof gateEnforceMfa>>>;
|
|
6
7
|
export declare function createSwitchTenantMfaGateHandler(opts: Pick<LoginHandlerOptions, "mfaStatusChecker">): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"switch-tenant-mfa-gate", z.ZodObject<{
|
|
7
8
|
userId: z.ZodString;
|
|
@@ -33,7 +33,7 @@ export function createSwitchTenantMfaGateHandler(opts) {
|
|
|
33
33
|
const userRow = parseAuthUserRow(await ctx.queryAs(createSystemUser(tenantId), UserQueries.findForAuth, { id: userId }));
|
|
34
34
|
const roles = buildSessionRoles(parseRoles(userRow?.roles ?? null), active.membership.roles);
|
|
35
35
|
const mfaGate = await gateEnforceMfa(ctx, opts, userId, tenantId, roles);
|
|
36
|
-
return { isSuccess: true, data: mfaGate ?? { kind: "mfa-gate-clear" } };
|
|
36
|
+
return { isSuccess: true, data: mfaGate ?? { kind: "mfa-gate-clear", roles } };
|
|
37
37
|
},
|
|
38
38
|
});
|
|
39
39
|
}
|
|
@@ -37,21 +37,24 @@ export async function dispatchMagicLinkMail(notify, spec, params) {
|
|
|
37
37
|
// mailT already root-falls-back; appUrl must use the same negotiated tag
|
|
38
38
|
// or language-in-path apps 404 on e.g. /de-AT/activate.
|
|
39
39
|
const urlLocale = resolveMailLocale(locale);
|
|
40
|
+
// Template, expiry formatting and app-name fallback must share one locale,
|
|
41
|
+
// otherwise an English template shows e.g. French "3 heures".
|
|
42
|
+
const templateLocale = authMailTemplateLocale(spec.notificationType, locale);
|
|
40
43
|
const { validFor, validUntil } = tokenMailExpiry({
|
|
41
44
|
expiresAt: params.expiresAt,
|
|
42
45
|
...(params.issuedAt !== undefined && { issuedAt: params.issuedAt }),
|
|
43
46
|
...(params.timeZone !== undefined && { timeZone: params.timeZone }),
|
|
44
|
-
},
|
|
47
|
+
}, templateLocale);
|
|
45
48
|
await notify(spec.notificationType, {
|
|
46
49
|
route: { email: params.email },
|
|
47
50
|
data: {
|
|
48
|
-
appName: params.appName ?? spec.fallbackAppName ?? mailT(
|
|
51
|
+
appName: params.appName ?? spec.fallbackAppName ?? mailT(templateLocale, "auth.mail.appNameDefault"),
|
|
49
52
|
actionUrl: appendToken(resolveAppUrl(params.appUrl, urlLocale), params.token),
|
|
50
53
|
validFor,
|
|
51
54
|
validUntil,
|
|
52
55
|
...(params.role !== undefined && { role: params.role }),
|
|
53
56
|
},
|
|
54
|
-
locale:
|
|
57
|
+
locale: templateLocale,
|
|
55
58
|
priority: "critical",
|
|
56
59
|
});
|
|
57
60
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// Server-registered so the document designer names the seeded auth mails.
|
|
3
|
+
export const AUTH_EMAIL_PASSWORD_MAIL_I18N = {
|
|
4
|
+
"auth-email-password:feature.label": { en: "Sign-in" },
|
|
5
|
+
"auth-email-password:notify.password-reset.label": { en: "Password reset" },
|
|
6
|
+
"auth-email-password:notify.account-unlock.label": { en: "Account unlock" },
|
|
7
|
+
"auth-email-password:notify.email-verification.label": { en: "Email verification" },
|
|
8
|
+
"auth-email-password:notify.signup-activation.label": { en: "Sign-up activation" },
|
|
9
|
+
"auth-email-password:notify.invite.label": { en: "Invitation" },
|
|
10
|
+
};
|
|
@@ -166,7 +166,14 @@ export function createRecordContractTerminationHandler(options, consumerProtecti
|
|
|
166
166
|
effectiveAtIso,
|
|
167
167
|
providerCancel,
|
|
168
168
|
};
|
|
169
|
-
|
|
169
|
+
// Re-fetch after the external call: a webhook appended meanwhile would
|
|
170
|
+
// otherwise fail the append on a stale expectedVersion although the
|
|
171
|
+
// provider already cancelled.
|
|
172
|
+
const streamAfterProviderCall = await ctx.fetchForWriting({
|
|
173
|
+
aggregateId: subscriptionAggregateId(tenantId),
|
|
174
|
+
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
175
|
+
});
|
|
176
|
+
await streamAfterProviderCall.appendOne({
|
|
170
177
|
type: CONTRACT_TERMINATION_REQUESTED_EVENT_QN,
|
|
171
178
|
payload: recorded,
|
|
172
179
|
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export const BILLING_FOUNDATION_I18N = {
|
|
2
|
+
"billing-foundation:feature.label": { en: "Billing" },
|
|
3
|
+
"billing-foundation:notify.contract-confirmation.label": { en: "Contract confirmation" },
|
|
4
|
+
"billing-foundation:notify.contract-termination-receipt.label": { en: "Termination receipt" },
|
|
5
|
+
"billing-foundation:notify.contract-termination-operator-notice.label": {
|
|
6
|
+
en: "Termination notice to operator",
|
|
7
|
+
},
|
|
2
8
|
"screen:billing-plans.title": { en: "Billing" },
|
|
3
9
|
"billing-foundation.plans.billingDisabled": {
|
|
4
10
|
en: "Billing is not active yet. Your current plan stays in place.",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// near-identical webhook-route.ts app copies in show-pony and publicstatus
|
|
3
3
|
// (infra#446) — the only per-app variables were the TierName union/default,
|
|
4
4
|
// so it is now a factory parameter.
|
|
5
|
-
import { TierAssignmentSources, TierEngineHandlers, TierEngineQueries, tierAssignmentAggregateId, } from "@cosmicdrift/kumiko-bundled-features/tier-engine";
|
|
5
|
+
import { isTierAssignmentSource, TierAssignmentSources, TierEngineHandlers, TierEngineQueries, tierAssignmentAggregateId, } from "@cosmicdrift/kumiko-bundled-features/tier-engine";
|
|
6
6
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
7
7
|
import { subscriptionAggregateId } from "./aggregate-id.js";
|
|
8
8
|
import { SubscriptionFoundationQueries, SubscriptionStatuses } from "./constants.js";
|
|
@@ -32,7 +32,13 @@ function findTierAssignment(rows, tenantId) {
|
|
|
32
32
|
if (!row || typeof row["id"] !== "string" || typeof row["version"] !== "number") {
|
|
33
33
|
return undefined;
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
const source = row["source"];
|
|
36
|
+
return {
|
|
37
|
+
id: row["id"],
|
|
38
|
+
version: row["version"],
|
|
39
|
+
tier: row["tier"],
|
|
40
|
+
source: isTierAssignmentSource(source) ? source : null,
|
|
41
|
+
};
|
|
36
42
|
}
|
|
37
43
|
export function createSubscriptionTierSync(deps) {
|
|
38
44
|
async function syncTierFromSubscription(tenantId, routeDeps) {
|
|
@@ -173,6 +173,11 @@ export async function markCapSoftWarned(ctx, options) {
|
|
|
173
173
|
}
|
|
174
174
|
return retryCounterWriteOnVersionConflict(() => attemptMark(writeDb));
|
|
175
175
|
}
|
|
176
|
+
// A release is written up to a reservation TTL after its increment; once the increment left the
|
|
177
|
+
// window the release must not subtract from other increments still inside it.
|
|
178
|
+
function isBefore(isoInstant, cutoff) {
|
|
179
|
+
return Temporal.Instant.compare(Temporal.Instant.from(isoInstant), cutoff) < 0;
|
|
180
|
+
}
|
|
176
181
|
async function sumRollingEventAmounts(db, tenantId, options, type) {
|
|
177
182
|
const aggregateId = rollingCapAggregateId(tenantId, options.capName);
|
|
178
183
|
const cutoff = Temporal.Now.instant().subtract({ hours: options.windowDays * 24 });
|
|
@@ -189,6 +194,9 @@ async function sumRollingEventAmounts(db, tenantId, options, type) {
|
|
|
189
194
|
// unknown by drizzle's $type<Record<string,unknown>>); narrowing the
|
|
190
195
|
// shape here mirrors enforceRollingCap's own read-side contract.
|
|
191
196
|
const payload = row["payload"];
|
|
197
|
+
if (typeof payload.incrementedAt === "string" && isBefore(payload.incrementedAt, cutoff)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
192
200
|
if (typeof payload.amount === "number") {
|
|
193
201
|
total += payload.amount;
|
|
194
202
|
}
|
|
@@ -61,13 +61,13 @@ export async function reserveCalendarCap(ctx, options, outsideTransaction = true
|
|
|
61
61
|
counterValue: valueBeforeBooking + options.amount,
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
function rollingEvent(user, capName, amount, expectedVersion, type) {
|
|
64
|
+
function rollingEvent(user, capName, amount, expectedVersion, type, extraPayload = {}) {
|
|
65
65
|
return {
|
|
66
66
|
aggregateId: rollingCapAggregateId(user.tenantId, capName),
|
|
67
67
|
aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
|
|
68
68
|
expectedVersion,
|
|
69
69
|
type,
|
|
70
|
-
payload: { capName, amount },
|
|
70
|
+
payload: { capName, amount, ...extraPayload },
|
|
71
71
|
metadata: { userId: user.id },
|
|
72
72
|
};
|
|
73
73
|
}
|
|
@@ -104,7 +104,11 @@ async function giveBackCalendar(txDb, user, row) {
|
|
|
104
104
|
}
|
|
105
105
|
async function giveBackRolling(txDb, user, row) {
|
|
106
106
|
const version = await getStreamVersionInTenantDb(txDb, rollingCapAggregateId(user.tenantId, row.capName));
|
|
107
|
-
await appendEventInTenantDb(txDb,
|
|
107
|
+
await appendEventInTenantDb(txDb,
|
|
108
|
+
// The window filter must use the increment's time: a sweep releases up to a TTL later.
|
|
109
|
+
rollingEvent(user, row.capName, row.amount, version, ROLLING_RELEASED_EVENT_QN, {
|
|
110
|
+
incrementedAt: row.createdAt.toString(),
|
|
111
|
+
}));
|
|
108
112
|
return NOTHING_TO_RELEASE;
|
|
109
113
|
}
|
|
110
114
|
async function giveBackReservation(txDb, user, id) {
|
|
@@ -10,5 +10,6 @@ export declare const rollingIncrementedSchema: z.ZodObject<{
|
|
|
10
10
|
export declare const rollingReleasedSchema: z.ZodObject<{
|
|
11
11
|
capName: z.ZodString;
|
|
12
12
|
amount: z.ZodNumber;
|
|
13
|
+
incrementedAt: z.ZodOptional<z.ZodString>;
|
|
13
14
|
}, z.core.$strip>;
|
|
14
15
|
export declare const incrementRollingCapHandler: WriteHandlerDef;
|
|
@@ -34,6 +34,8 @@ export const rollingIncrementedSchema = incrementRollingSchema;
|
|
|
34
34
|
/** Payload of the event that gives a reserved rolling amount back. */
|
|
35
35
|
export const rollingReleasedSchema = incrementRollingSchema.extend({
|
|
36
36
|
amount: z.number().int().positive(),
|
|
37
|
+
/** When the released increment was booked; absent on events written before it was recorded. */
|
|
38
|
+
incrementedAt: z.string().optional(),
|
|
37
39
|
});
|
|
38
40
|
// Rolling-Increment-Handler — append-only. Race-frei: zwei parallele
|
|
39
41
|
// Increments für (tenant, cap) hängen sich am selben aggregate-stream
|
|
@@ -116,7 +116,9 @@ export function createTenantCapsListQuery(caps, listCaps) {
|
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
118
|
const db = ctx.systemDb.acknowledgeCrossTenant("cap-overview:tenant-caps:list — SystemAdmin platform-wide tenant overview");
|
|
119
|
-
const tenants = await decryptTenantNames(
|
|
119
|
+
const tenants = await decryptTenantNames(
|
|
120
|
+
// Destroyed tenants decrypt to the erased sentinel, which a picker or list would show verbatim.
|
|
121
|
+
await selectMany(db, tenantTable, { destroyedAt: null }), "cap-overview:tenant-caps:list");
|
|
120
122
|
const assignments = await selectMany(db, tierAssignmentTable, {});
|
|
121
123
|
const subscriptions = await selectMany(db, subscriptionsProjectionTable, {});
|
|
122
124
|
const assignmentByTenant = new Map(assignments.map((row) => [row.tenantId, row]));
|
|
@@ -15,7 +15,9 @@ export const tenantOptionsQuery = defineQueryHandler({
|
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
const db = ctx.systemDb.acknowledgeCrossTenant("cap-overview:tenant-options — SystemAdmin dashboard tenant-filter options");
|
|
18
|
-
const tenants = await decryptTenantNames(
|
|
18
|
+
const tenants = await decryptTenantNames(
|
|
19
|
+
// Destroyed tenants decrypt to the erased sentinel, which the picker would show verbatim.
|
|
20
|
+
await selectMany(db, tenantTable, { destroyedAt: null }), "cap-overview:tenant-options");
|
|
19
21
|
const sorted = [...tenants].sort((a, b) => a.name.localeCompare(b.name));
|
|
20
22
|
return { rows: sorted.map((tenant) => ({ value: tenant.id, label: tenant.name })) };
|
|
21
23
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { chatMessageText, createChatWebhookChannel, truncateChars, } from "../delivery/index.js";
|
|
2
|
+
const DISCORD_WEBHOOK_PREFIX = "channel-discord:webhooks.";
|
|
2
3
|
export const DISCORD_SECRET_KEYS = {
|
|
3
|
-
webhookPrefix:
|
|
4
|
-
webhookKeyFor: (connection) =>
|
|
4
|
+
webhookPrefix: DISCORD_WEBHOOK_PREFIX,
|
|
5
|
+
webhookKeyFor: (connection) => `${DISCORD_WEBHOOK_PREFIX}${connection}`,
|
|
5
6
|
};
|
|
6
7
|
export const DISCORD_DEFAULT_ALLOWED_HOSTS = ["discord.com", "discordapp.com"];
|
|
7
8
|
export const DISCORD_WEBHOOK_PATH_PREFIX = "/api/webhooks/";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
|
|
2
|
+
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
2
3
|
import * as z from "zod";
|
|
3
4
|
import { DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE, DELIVERY_UNSUBSCRIBE_PATH, EMAIL_CHANNEL_NAME, fileAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, renderedPdfAttachmentRefSchema, } from "../delivery/index.js";
|
|
4
|
-
import { decryptStoredPii } from "../shared/index.js";
|
|
5
|
+
import { decryptStoredPii, extensionForResourceContentType } from "../shared/index.js";
|
|
5
6
|
import { guardEmailMessage } from "./pii-guard.js";
|
|
6
7
|
// Envelope (From / Reply-To / threading headers) rides on the channel data —
|
|
7
8
|
// the notification's email template echoes it out of the notify() call, since
|
|
@@ -22,7 +23,8 @@ const UNSUBSCRIBE_HEADER_NAMES = new Set([
|
|
|
22
23
|
"list-unsubscribe",
|
|
23
24
|
"list-unsubscribe-post",
|
|
24
25
|
]);
|
|
25
|
-
// Only the framework's own unsubscribe route can honor a one-click POST
|
|
26
|
+
// Only the framework's own unsubscribe route can honor a one-click POST; an app's
|
|
27
|
+
// own unsubscribe page still gets the plain List-Unsubscribe link (RFC 2369).
|
|
26
28
|
function listUnsubscribeHeaders(data) {
|
|
27
29
|
const rawUrl = data?.["unsubscribeUrl"];
|
|
28
30
|
if (typeof rawUrl !== "string")
|
|
@@ -36,8 +38,9 @@ function listUnsubscribeHeaders(data) {
|
|
|
36
38
|
}
|
|
37
39
|
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
38
40
|
return undefined;
|
|
39
|
-
if (url.pathname !== DELIVERY_UNSUBSCRIBE_PATH)
|
|
40
|
-
return
|
|
41
|
+
if (url.pathname !== DELIVERY_UNSUBSCRIBE_PATH) {
|
|
42
|
+
return { "List-Unsubscribe": `<${url.toString()}>` };
|
|
43
|
+
}
|
|
41
44
|
return {
|
|
42
45
|
"List-Unsubscribe": `<${url.toString()}>`,
|
|
43
46
|
"List-Unsubscribe-Post": DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE,
|
|
@@ -81,15 +84,13 @@ export function fileAttachmentsFrom(data, messageLocale) {
|
|
|
81
84
|
? { ...ref, locale: messageLocale }
|
|
82
85
|
: ref);
|
|
83
86
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"image/webp": "webp",
|
|
89
|
-
};
|
|
87
|
+
function inlineFileName(cid, contentType) {
|
|
88
|
+
const extension = extensionForResourceContentType(contentType);
|
|
89
|
+
return extension === undefined ? cid : `${cid}.${extension}`;
|
|
90
|
+
}
|
|
90
91
|
async function resolveAttachments(sources, ctx) {
|
|
91
92
|
if (sources.length === 0)
|
|
92
|
-
return [];
|
|
93
|
+
return { kind: "resolved", attachments: [] };
|
|
93
94
|
if (ctx.readAttachment === undefined) {
|
|
94
95
|
throw new Error("email channel: the message has attachments but the channel context cannot read files");
|
|
95
96
|
}
|
|
@@ -113,16 +114,12 @@ async function resolveAttachments(sources, ctx) {
|
|
|
113
114
|
// A missing inline image only degrades the look; a missing file is content the recipient expects.
|
|
114
115
|
if (ref.kind === "inline")
|
|
115
116
|
continue;
|
|
116
|
-
|
|
117
|
+
return { kind: "missing-file", fileRefId: ref.fileRefId };
|
|
117
118
|
}
|
|
118
|
-
const fileName = ref.kind === "file"
|
|
119
|
-
? ref.fileName
|
|
120
|
-
: INLINE_EXTENSION_BY_CONTENT_TYPE[found.contentType]
|
|
121
|
-
? `${ref.cid}.${INLINE_EXTENSION_BY_CONTENT_TYPE[found.contentType]}`
|
|
122
|
-
: ref.cid;
|
|
119
|
+
const fileName = ref.kind === "file" ? ref.fileName : inlineFileName(ref.cid, found.contentType);
|
|
123
120
|
resolved.push({ ...ref, fileName, content: found.content, contentType: found.contentType });
|
|
124
121
|
}
|
|
125
|
-
return resolved;
|
|
122
|
+
return { kind: "resolved", attachments: resolved };
|
|
126
123
|
}
|
|
127
124
|
export function createEmailChannel(options) {
|
|
128
125
|
const { transport, resolveEmail } = options;
|
|
@@ -175,10 +172,18 @@ export function createEmailChannel(options) {
|
|
|
175
172
|
...(replyTo && { replyTo }),
|
|
176
173
|
...emailEnvelopeFrom(message.data),
|
|
177
174
|
};
|
|
178
|
-
const
|
|
175
|
+
const resolution = await resolveAttachments([
|
|
179
176
|
...(attachments ?? []).map((ref) => ({ ref, origin: "template" })),
|
|
180
177
|
...fileAttachmentsFrom(message.data, message.locale).map((ref) => ({ ref, origin: "message" })),
|
|
181
178
|
], ctx);
|
|
179
|
+
if (resolution.kind === "missing-file") {
|
|
180
|
+
// A missing file is permanent, a retry cannot bring it back.
|
|
181
|
+
createFallbackLogger("delivery").warn("mail attachment file not found, send failed", {
|
|
182
|
+
fileRefId: resolution.fileRefId,
|
|
183
|
+
});
|
|
184
|
+
return { status: "failed", error: "send_failed", address };
|
|
185
|
+
}
|
|
186
|
+
const resolvedAttachments = resolution.attachments;
|
|
182
187
|
await transport.send(guardEmailMessage({
|
|
183
188
|
to: address,
|
|
184
189
|
subject,
|
|
@@ -21,11 +21,16 @@ function isControlCharacter(char) {
|
|
|
21
21
|
const code = char.charCodeAt(0);
|
|
22
22
|
return code <= 0x1f || code === 0x7f;
|
|
23
23
|
}
|
|
24
|
+
// Mail clients often show only the display name: an embedded address ("<", ">", "@") would
|
|
25
|
+
// pass for a foreign sender on the platform's own SPF/DKIM-signed domain.
|
|
26
|
+
function isUnsafeDisplayNameCharacter(char) {
|
|
27
|
+
return isControlCharacter(char) || char === "<" || char === ">" || char === "@";
|
|
28
|
+
}
|
|
24
29
|
// Control characters in a display name would let a caller inject headers.
|
|
25
30
|
function sanitizedDisplayName(name) {
|
|
26
31
|
const cleaned = name
|
|
27
32
|
?.split("")
|
|
28
|
-
.map((char) => (
|
|
33
|
+
.map((char) => (isUnsafeDisplayNameCharacter(char) ? " " : char))
|
|
29
34
|
.join("")
|
|
30
35
|
.trim();
|
|
31
36
|
return cleaned === undefined || cleaned === "" ? undefined : cleaned;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { chatMessageText, createChatWebhookChannel, } from "../delivery/index.js";
|
|
2
|
+
const SLACK_WEBHOOK_PREFIX = "channel-slack:webhooks.";
|
|
2
3
|
export const SLACK_SECRET_KEYS = {
|
|
3
|
-
webhookPrefix:
|
|
4
|
-
webhookKeyFor: (connection) =>
|
|
4
|
+
webhookPrefix: SLACK_WEBHOOK_PREFIX,
|
|
5
|
+
webhookKeyFor: (connection) => `${SLACK_WEBHOOK_PREFIX}${connection}`,
|
|
5
6
|
};
|
|
6
7
|
export const SLACK_DEFAULT_ALLOWED_HOSTS = ["hooks.slack.com"];
|
|
7
8
|
// Slack mrkdwn treats & < > as control characters (links, @-mentions via <!channel>).
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createChatWebhookChannel, } from "../delivery/index.js";
|
|
2
|
+
const TEAMS_WEBHOOK_PREFIX = "channel-teams:webhooks.";
|
|
2
3
|
export const TEAMS_SECRET_KEYS = {
|
|
3
|
-
webhookPrefix:
|
|
4
|
-
webhookKeyFor: (connection) =>
|
|
4
|
+
webhookPrefix: TEAMS_WEBHOOK_PREFIX,
|
|
5
|
+
webhookKeyFor: (connection) => `${TEAMS_WEBHOOK_PREFIX}${connection}`,
|
|
5
6
|
};
|
|
6
7
|
export const TEAMS_DEFAULT_ALLOWED_HOSTS = [
|
|
7
8
|
".webhook.office.com",
|
|
@@ -2,7 +2,7 @@ import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
|
2
2
|
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
3
3
|
import { fetchOne, runInSavepointIfSupported, runnerCanBeginTransaction, transaction, } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
4
4
|
import { computeBlindIndex, configuredBlindIndexKey, configuredPiiSubjectKms, subjectIdSchema, subjectIdToKey, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
5
|
-
import { nullBlindIndexesForSubject, recordEventsOwnedExclusivelyByTenant, recordRowExistsInTenant, recordRowOwningTenantId, subjectRowExistsInTenant, } from "@cosmicdrift/kumiko-framework/db";
|
|
5
|
+
import { nullBlindIndexesForSubject, recordEventsOwnedExclusivelyByTenant, recordRowExistsInTenant, recordRowOwningTenantId, subjectRowExistsInTenant, WritePurposes, } from "@cosmicdrift/kumiko-framework/db";
|
|
6
6
|
import { defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
7
|
import { AccessDeniedError, InternalError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
8
8
|
import { appendDomainEventCore, assertIrreversibleOperationAllowed, } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
@@ -254,7 +254,7 @@ async function closeUserLoginDoor(ctx, userId, outsideRunner, handlerTxRunner) {
|
|
|
254
254
|
if (!userRow)
|
|
255
255
|
return;
|
|
256
256
|
const closeDoor = async (runner) => {
|
|
257
|
-
await updateUserLifecycle(runner, userId, { status: USER_STATUS.Deleted });
|
|
257
|
+
await updateUserLifecycle(runner, userId, { status: USER_STATUS.Deleted }, { writePurpose: WritePurposes.erasure });
|
|
258
258
|
if (ctx.registry.features.has("personal-access-tokens")) {
|
|
259
259
|
await revokeAllPatTokensForUser(runner, userId);
|
|
260
260
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
export declare function selectHostRowsWithCustomFields(db: DbRunner, tableName: string, tenantId: string): Promise<readonly unknown[]>;
|
|
3
3
|
export declare function applyRetentionRemovals(db: DbRunner, tableName: string, deleteKeys: readonly string[], anonymizeKeys: readonly string[], rowId: string): Promise<void>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { asRawClient, unsafeReadRetrying } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { toPostgresArrayLiteral } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
export async function selectHostRowsWithCustomFields(db, tableName, tenantId) {
|
|
3
4
|
const quoted = `"${tableName.replace(/"/g, '""')}"`;
|
|
4
5
|
const rowsResult = await unsafeReadRetrying(db, `SELECT id, modified_at, custom_fields FROM ${quoted} WHERE tenant_id = $1 AND custom_fields IS NOT NULL`, [tenantId]);
|
|
@@ -19,5 +20,5 @@ export async function applyRetentionRemovals(db, tableName, deleteKeys, anonymiz
|
|
|
19
20
|
)
|
|
20
21
|
ELSE custom_fields
|
|
21
22
|
END
|
|
22
|
-
WHERE id = $3`, [deleteKeys, anonymizeKeys, rowId]);
|
|
23
|
+
WHERE id = $3`, [toPostgresArrayLiteral(deleteKeys), toPostgresArrayLiteral(anonymizeKeys), rowId]);
|
|
23
24
|
}
|