@cosmicdrift/kumiko-samples 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/LICENSE +1 -1
- package/package.json +1 -1
- package/packages/bundled-features/package.json +1 -1
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +63 -11
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +1 -3
- package/packages/bundled-features/src/auth-email-password/changes.json +18 -0
- package/packages/bundled-features/src/auth-email-password/feature.ts +4 -7
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +6 -4
- package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +2 -2
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +7 -3
- package/packages/bundled-features/src/auth-email-password/mail-i18n.ts +10 -0
- package/packages/bundled-features/src/billing-foundation/changes.json +12 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +8 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +6 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +10 -2
- package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +20 -8
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +7 -2
- package/packages/bundled-features/src/cap-counter/changes.json +13 -0
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +2 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +2 -1
- package/packages/bundled-features/src/channel-discord/discord-channel.ts +4 -2
- package/packages/bundled-features/src/channel-email/changes.json +18 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +30 -20
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +7 -1
- package/packages/bundled-features/src/channel-slack/slack-channel.ts +4 -2
- package/packages/bundled-features/src/channel-teams/teams-channel.ts +4 -2
- package/packages/bundled-features/src/crypto-shredding/changes.json +7 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +7 -1
- package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +2 -2
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +4 -5
- package/packages/bundled-features/src/delivery/attachment-ref.ts +18 -1
- package/packages/bundled-features/src/delivery/changes.json +12 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +93 -92
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/feature-toggles/changes.json +7 -0
- package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +26 -12
- package/packages/bundled-features/src/files-tenant-data/index.ts +3 -1
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +12 -3
- package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
- package/packages/bundled-features/src/jobs/changes.json +12 -0
- package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -2
- package/packages/bundled-features/src/metrics/changes.json +6 -0
- package/packages/bundled-features/src/metrics/filters.ts +28 -12
- package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +7 -8
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/handlers/delete.write.ts +3 -1
- package/packages/bundled-features/src/secrets/write-gate.ts +8 -2
- package/packages/bundled-features/src/shared/index.ts +8 -0
- package/packages/bundled-features/src/shared/resource-image-formats.ts +31 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +7 -8
- package/packages/bundled-features/src/step-dispatcher/changes.json +8 -1
- package/packages/bundled-features/src/step-dispatcher/constants.ts +22 -0
- package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +236 -0
- package/packages/bundled-features/src/step-dispatcher/feature.ts +65 -118
- package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +40 -14
- package/packages/bundled-features/src/template-resolver/changes.json +81 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +78 -12
- package/packages/bundled-features/src/template-resolver/compile-steps-block.ts +68 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +11 -1
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +7 -1
- package/packages/bundled-features/src/template-resolver/document-steps.ts +114 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +12 -0
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +81 -39
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +4 -3
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +111 -61
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +1 -0
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +25 -15
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +12 -1
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +12 -2
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +48 -9
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +4 -1
- package/packages/bundled-features/src/template-resolver/index.ts +6 -1
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +25 -9
- package/packages/bundled-features/src/template-resolver/mail-constants.ts +4 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +9 -16
- package/packages/bundled-features/src/template-resolver/seeding.ts +2 -0
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +390 -124
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +4 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +1 -1
- package/packages/bundled-features/src/template-resolver/template-meta.ts +8 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +1 -1
- package/packages/bundled-features/src/template-resolver/template-resources.ts +2 -43
- package/packages/bundled-features/src/template-resolver/template-validation.ts +15 -9
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +34 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +3 -2
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +56 -11
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +6 -3
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +1 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +14 -5
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +3 -8
- package/packages/bundled-features/src/tenant/seeding.ts +33 -27
- package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +14 -3
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +9 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +9 -3
- package/packages/bundled-features/src/tier-engine/changes.json +12 -0
- package/packages/bundled-features/src/tier-engine/constants.ts +9 -0
- package/packages/bundled-features/src/tier-engine/entity.ts +9 -4
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +8 -5
- package/packages/bundled-features/src/tier-engine/index.ts +2 -0
- package/packages/bundled-features/src/user/constants.ts +1 -0
- package/packages/bundled-features/src/user/invited-global-roles.ts +2 -4
- package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +0 -1
- package/packages/bundled-features/src/user-data-rights/i18n.ts +5 -0
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +13 -2
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +7 -1
- package/packages/bundled-features/src/waitlist/changes.json +18 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -1
- package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +35 -3
- package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +2 -17
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +33 -9
- package/packages/bundled-features/src/waitlist/i18n.ts +9 -0
- package/packages/bundled-features/src/waitlist/lib.ts +20 -0
- package/packages/bundled-features/src/workflow-runner/feature.ts +1 -1
- package/samples/apps/ui-walkthrough/src/run-config.ts +14 -1
- package/samples/apps/use-all-bundled/README.md +3 -13
- package/samples/apps/use-all-bundled/feature-manifest.json +4 -4
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +34 -118
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +1 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +0 -3
- package/samples/recipes/auth-mfa/README.md +2 -1
- package/samples/recipes/auth-mfa/src/feature.ts +1 -0
- package/samples/recipes/custom-handlers/README.md +4 -3
- package/samples/recipes/custom-handlers/src/feature.ts +21 -12
- package/samples/recipes/dashboard-stats/README.md +38 -0
- package/samples/recipes/dashboard-stats/package.json +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +104 -0
- package/samples/recipes/legal-pages/README.md +2 -2
- package/samples/recipes/managed-pages/README.md +2 -2
- package/samples/recipes/pdf-audit-export/README.md +2 -1
- package/samples/recipes/pdf-audit-export/package.json +1 -1
- package/samples/recipes/pdf-audit-export/src/feature.ts +68 -48
- package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +2 -2
- package/samples/apps/use-all-bundled/schema/generate.ts +0 -617
- package/samples/apps/use-all-bundled/schema/migrations/meta/0000_snapshot.json +0 -3055
- package/samples/apps/use-all-bundled/schema/migrations/meta/_journal.json +0 -13
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +0 -94
- package/samples/apps/use-all-bundled/schema/schema.ts +0 -9
package/LICENSE
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-samples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.365.1",
|
|
4
4
|
"description": "Source trees of the Kumiko sample recipes, sample apps and bundled features in repo layout, for tooling such as few-shot corpus builds.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.365.1",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Escape-hatch retention keeps the operator floor and deletes in bounded batches",
|
|
6
|
+
"detail": "An explicitly configured retention key now acts as a lower bound for tenant compliance-profile cutoffs, so a tenant profile can no longer delete events younger than the operator allows. A key left at its default does not count. Pruning runs in batches of 5000 events, each in its own transaction, until a batch comes back short."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.365.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Audit retention events always use the system user",
|
|
12
|
+
"detail": "Events written by the audit feature no longer fall back to the calling user id and always record the system user."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.364.0",
|
|
4
16
|
"type": "fix",
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
aggregateWhere,
|
|
3
|
+
runnerCanBeginTransaction,
|
|
4
|
+
selectMany,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
6
|
import {
|
|
3
7
|
COMPLIANCE_PROFILES,
|
|
4
8
|
subtractRetentionSpec,
|
|
@@ -27,7 +31,7 @@ import {
|
|
|
27
31
|
|
|
28
32
|
// pruneEvents opens its own transaction, so a runner that cannot begin one (a tx handle) is rejected instead of nested.
|
|
29
33
|
export function isDbConnection(runner: DbRunner): runner is DbConnection {
|
|
30
|
-
return
|
|
34
|
+
return runnerCanBeginTransaction(runner);
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
async function resolveRetentionDays(args: {
|
|
@@ -48,9 +52,30 @@ async function resolveRetentionDays(args: {
|
|
|
48
52
|
return typeof raw === "number" && raw >= 1 ? raw : DEFAULT_ESCAPE_HATCH_RETENTION_DAYS;
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
// Only an operator-set value counts: the key's declared default must not override the tenant profiles.
|
|
56
|
+
async function resolveExplicitRetentionDays(args: {
|
|
57
|
+
readonly registry: Registry;
|
|
58
|
+
readonly configResolver: ConfigResolver | undefined;
|
|
59
|
+
readonly db: DbConnection;
|
|
60
|
+
readonly userId: string;
|
|
61
|
+
}): Promise<number | undefined> {
|
|
62
|
+
const keyDef = args.registry.getConfigKey(ESCAPE_HATCH_RETENTION_DAYS_KEY);
|
|
63
|
+
if (!args.configResolver || !keyDef) return undefined;
|
|
64
|
+
const { value, source } = await args.configResolver.getWithSource(
|
|
65
|
+
ESCAPE_HATCH_RETENTION_DAYS_KEY,
|
|
66
|
+
keyDef,
|
|
67
|
+
SYSTEM_TENANT_ID,
|
|
68
|
+
args.userId,
|
|
69
|
+
args.db,
|
|
70
|
+
);
|
|
71
|
+
const isOperatorSet = source !== "default" && source !== "missing";
|
|
72
|
+
return isOperatorSet && typeof value === "number" && value >= 1 ? value : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
51
75
|
// Soft dependency: audit must stay mountable without compliance-profiles, so we probe its entity.
|
|
52
76
|
const COMPLIANCE_PROFILE_ENTITY = "tenant-compliance-profile";
|
|
53
77
|
const PRUNE_CHUNK_SIZE = 1000;
|
|
78
|
+
const PRUNE_BATCH_SIZE = 5000;
|
|
54
79
|
|
|
55
80
|
type AuditEventRow = {
|
|
56
81
|
readonly aggregateId: string;
|
|
@@ -68,11 +93,13 @@ async function resolveTenantCutoff(
|
|
|
68
93
|
db: DbConnection,
|
|
69
94
|
tenantId: TenantId,
|
|
70
95
|
now: Temporal.Instant,
|
|
96
|
+
operatorFloor: Temporal.Instant | undefined,
|
|
71
97
|
): Promise<Temporal.Instant> {
|
|
72
98
|
const { profile } = await resolveProfileForTenant({ db, tenantId });
|
|
73
99
|
const effective = subtractRetentionSpec(now, profile.auditLog.retention);
|
|
74
100
|
const base = subtractRetentionSpec(now, COMPLIANCE_PROFILES[profile.key].auditLog.retention);
|
|
75
|
-
|
|
101
|
+
const profileCutoff = earlierInstant(effective, base);
|
|
102
|
+
return operatorFloor ? earlierInstant(profileCutoff, operatorFloor) : profileCutoff;
|
|
76
103
|
}
|
|
77
104
|
|
|
78
105
|
// A cross-tenant audit (e.g. identity-switch) lives under the actor's tenant but must also
|
|
@@ -95,13 +122,20 @@ async function storingTenantIds(db: DbConnection): Promise<readonly TenantId[]>
|
|
|
95
122
|
});
|
|
96
123
|
}
|
|
97
124
|
|
|
98
|
-
async function pruneByTenantProfiles(
|
|
125
|
+
async function pruneByTenantProfiles(
|
|
126
|
+
db: DbConnection,
|
|
127
|
+
operatorRetentionDays: number | undefined,
|
|
128
|
+
): Promise<number> {
|
|
99
129
|
const now = getTemporal().Now.instant();
|
|
130
|
+
const operatorFloor =
|
|
131
|
+
operatorRetentionDays === undefined
|
|
132
|
+
? undefined
|
|
133
|
+
: now.subtract({ hours: operatorRetentionDays * 24 });
|
|
100
134
|
const cutoffByTenant = new Map<TenantId, Temporal.Instant>();
|
|
101
135
|
const cutoffOf = async (tenantId: TenantId): Promise<Temporal.Instant> => {
|
|
102
136
|
const cached = cutoffByTenant.get(tenantId);
|
|
103
137
|
if (cached) return cached;
|
|
104
|
-
const cutoff = await resolveTenantCutoff(db, tenantId, now);
|
|
138
|
+
const cutoff = await resolveTenantCutoff(db, tenantId, now, operatorFloor);
|
|
105
139
|
cutoffByTenant.set(tenantId, cutoff);
|
|
106
140
|
return cutoff;
|
|
107
141
|
};
|
|
@@ -139,11 +173,32 @@ async function pruneByTenantProfiles(db: DbConnection): Promise<number> {
|
|
|
139
173
|
return deletedCount;
|
|
140
174
|
}
|
|
141
175
|
|
|
176
|
+
async function pruneInBatches(
|
|
177
|
+
db: DbConnection,
|
|
178
|
+
olderThanDays: number,
|
|
179
|
+
batchSize = PRUNE_BATCH_SIZE,
|
|
180
|
+
): Promise<number> {
|
|
181
|
+
const olderThan = getTemporal()
|
|
182
|
+
.Now.instant()
|
|
183
|
+
.subtract({ hours: olderThanDays * 24 });
|
|
184
|
+
let deletedCount = 0;
|
|
185
|
+
for (;;) {
|
|
186
|
+
const batch = await pruneEvents(db, {
|
|
187
|
+
aggregateTypes: [ESCAPE_HATCH_USE_AGGREGATE_TYPE],
|
|
188
|
+
olderThan,
|
|
189
|
+
batchSize,
|
|
190
|
+
});
|
|
191
|
+
deletedCount += batch.deletedCount;
|
|
192
|
+
if (batch.deletedCount < batchSize) return deletedCount;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
142
196
|
export async function runEscapeHatchRetention(args: {
|
|
143
197
|
readonly db: DbRunner;
|
|
144
198
|
readonly registry: Registry;
|
|
145
199
|
readonly configResolver: ConfigResolver | undefined;
|
|
146
200
|
readonly userId: string;
|
|
201
|
+
readonly batchSize?: number;
|
|
147
202
|
}): Promise<{ readonly deletedCount: number }> {
|
|
148
203
|
const { db } = args;
|
|
149
204
|
if (!isDbConnection(db)) {
|
|
@@ -152,12 +207,9 @@ export async function runEscapeHatchRetention(args: {
|
|
|
152
207
|
|
|
153
208
|
if (!args.registry.getEntity(COMPLIANCE_PROFILE_ENTITY)) {
|
|
154
209
|
const olderThanDays = await resolveRetentionDays({ ...args, db });
|
|
155
|
-
|
|
156
|
-
aggregateTypes: [ESCAPE_HATCH_USE_AGGREGATE_TYPE],
|
|
157
|
-
olderThanDays,
|
|
158
|
-
});
|
|
159
|
-
return { deletedCount };
|
|
210
|
+
return { deletedCount: await pruneInBatches(db, olderThanDays, args.batchSize) };
|
|
160
211
|
}
|
|
161
212
|
|
|
162
|
-
|
|
213
|
+
const operatorRetentionDays = await resolveExplicitRetentionDays({ ...args, db });
|
|
214
|
+
return { deletedCount: await pruneByTenantProfiles(db, operatorRetentionDays) };
|
|
163
215
|
}
|
|
@@ -36,14 +36,12 @@ import { tenantMembershipsTable } from "../tenant/membership-table.js";
|
|
|
36
36
|
// kumiko-lint-ignore cross-feature-import bootstrap checks for an existing tenant
|
|
37
37
|
import { tenantTable } from "../tenant/schema/tenant.js";
|
|
38
38
|
// kumiko-lint-ignore cross-feature-import global-role options are owned by the user feature
|
|
39
|
-
import type {
|
|
39
|
+
import type { GlobalRole } from "../user/constants.js";
|
|
40
40
|
// kumiko-lint-ignore cross-feature-import bootstrap skips already activated users
|
|
41
41
|
import { userTable } from "../user/schema/user.js";
|
|
42
42
|
import { AuthHandlers } from "./constants.js";
|
|
43
43
|
import { hasLiveInviteToken } from "./invite-token-store.js";
|
|
44
44
|
|
|
45
|
-
type GlobalRole = (typeof GLOBAL_ROLE_OPTIONS)[number];
|
|
46
|
-
|
|
47
45
|
export type BootstrapInvite = {
|
|
48
46
|
readonly email: string;
|
|
49
47
|
/** Membership role in the tenant, validated like a tenant-admin invite
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Magic-link mail expiry and app name use the template locale",
|
|
6
|
+
"detail": "The expiry text, the app-name fallback and the notify locale follow the locale the auth mail template actually renders in, while the link keeps the request locale."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.365.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "systemInviteCreate takes an optional locale tag",
|
|
12
|
+
"detail": "The payload accepts a locale (BCP 47 tag) that wins over the request locale when rendering the invitation mail, so system callers such as the waitlist can invite in the recipient's language."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.365.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "Auth mails carry designer labels",
|
|
18
|
+
"detail": "The feature registers <feature>:feature.label and notify labels for password-reset, account-unlock, email-verification, signup-activation and invite, so the document designer lists them by name. German and Spanish ship in the locale packages."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.364.0",
|
|
4
22
|
"type": "fix",
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "./handlers/signup-request.write.js";
|
|
31
31
|
import { createSwitchTenantMfaGateHandler } from "./handlers/switch-tenant-mfa-gate.write.js";
|
|
32
32
|
import { createVerifyEmailHandler } from "./handlers/verify-email.write.js";
|
|
33
|
+
import { AUTH_EMAIL_PASSWORD_MAIL_I18N } from "./mail-i18n.js";
|
|
33
34
|
import {
|
|
34
35
|
AUTH_MAIL_VARIABLES,
|
|
35
36
|
authMailLocalizedTemplate,
|
|
@@ -113,13 +114,8 @@ export type AccountLockoutOptions = {
|
|
|
113
114
|
readonly lockoutDurationMinutes?: number;
|
|
114
115
|
};
|
|
115
116
|
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// kommt aus ctx.redis — tokenTtlMinutes ist der einzige Knopf
|
|
119
|
-
// (Token-Material ist generateToken() = 256 Bit randomBytes, fest;
|
|
120
|
-
// Memory feedback_no_options_without_need: keine Knöpfe ohne Bedarf).
|
|
121
|
-
// Anders als reset/verify gibt's kein hmacSecret hier, weil der Token
|
|
122
|
-
// opaque random ist (Redis ist Source of Truth).
|
|
117
|
+
// Unlike reset/verify there is no hmacSecret: the signup token is opaque random
|
|
118
|
+
// (256 bit) and Redis is the source of truth, so tokenTtlMinutes is the only knob.
|
|
123
119
|
export type SignupOptions = SignupRequestOptions;
|
|
124
120
|
|
|
125
121
|
// Tenant-Invite Magic-Link. Wenn gesetzt, registriert das Feature die
|
|
@@ -207,6 +203,7 @@ export function createAuthEmailPasswordFeature(
|
|
|
207
203
|
// of silently dropping the mail.
|
|
208
204
|
if (sendsMagicLinkMail(opts)) r.requires("delivery");
|
|
209
205
|
r.envSchema(authEmailPasswordEnvSchema);
|
|
206
|
+
r.translations({ keys: AUTH_EMAIL_PASSWORD_MAIL_I18N });
|
|
210
207
|
|
|
211
208
|
// Declared unconditionally: apps also issue the activation link from their
|
|
212
209
|
// own flows (issueSignupActivation) without enabling the signup option.
|
package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts
CHANGED
|
@@ -121,9 +121,8 @@ export function createInviteAcceptWithLoginHandler(opts: InviteAcceptWithLoginOp
|
|
|
121
121
|
escapeHatch: {
|
|
122
122
|
grants: ["unsafeRaw"],
|
|
123
123
|
reason:
|
|
124
|
-
"Anonymous invite-accept has no session in the invited tenant yet —
|
|
125
|
-
"
|
|
126
|
-
"Also reads the pending invitation by id (the invitee is not yet a member of the " +
|
|
124
|
+
"Anonymous invite-accept has no session in the invited tenant yet — reads the pending " +
|
|
125
|
+
"invitation by id (the invitee is not yet a member of the " +
|
|
127
126
|
"invitation's tenant), adds the membership and accepts the invitation in the " +
|
|
128
127
|
"invitation's tenant, which differs from the caller's tenant, and reads the MFA " +
|
|
129
128
|
"enrollment of that tenant via the mfaStatusChecker callback.",
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type WriteFailure,
|
|
33
33
|
writeFailure,
|
|
34
34
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
35
|
+
import { isValidLocaleTag } from "@cosmicdrift/kumiko-framework/i18n";
|
|
35
36
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
36
37
|
import * as z from "zod";
|
|
37
38
|
// kumiko-lint-ignore cross-feature-import invite-flow lebt in auth-email-password (Magic-Link-Pattern), DB-row-owner ist tenant-feature
|
|
@@ -50,7 +51,7 @@ import {
|
|
|
50
51
|
unassignableMembershipRoleError,
|
|
51
52
|
} from "../../tenant/membership-roles.js";
|
|
52
53
|
// kumiko-lint-ignore cross-feature-import global-role options are owned by the user feature
|
|
53
|
-
import { GLOBAL_ROLE_OPTIONS } from "../../user/constants.js";
|
|
54
|
+
import { GLOBAL_ROLE_OPTIONS, type GlobalRole } from "../../user/constants.js";
|
|
54
55
|
// kumiko-lint-ignore cross-feature-import invitee lookup by email reads the user row
|
|
55
56
|
import { userTable } from "../../user/schema/user.js";
|
|
56
57
|
import { AUTH_INVITE_DEFAULT_TTL_MINUTES } from "../constants.js";
|
|
@@ -60,8 +61,6 @@ import { invalidateExistingInviteToken, storeInviteToken } from "../invite-token
|
|
|
60
61
|
import { dispatchMagicLinkMail, resolveHandlerMailLocale } from "../magic-link-mail.js";
|
|
61
62
|
import { AuthMailNotificationTypes } from "../mail-notifications.js";
|
|
62
63
|
|
|
63
|
-
type GlobalRole = (typeof GLOBAL_ROLE_OPTIONS)[number];
|
|
64
|
-
|
|
65
64
|
const InviteCreateSchema = z.object({
|
|
66
65
|
email: z.email(),
|
|
67
66
|
role: z.string().min(1).max(50),
|
|
@@ -116,6 +115,7 @@ type InviteIssueRequest = {
|
|
|
116
115
|
readonly email: string;
|
|
117
116
|
readonly role: string;
|
|
118
117
|
readonly globalRoles: readonly GlobalRole[];
|
|
118
|
+
readonly locale?: string | undefined;
|
|
119
119
|
};
|
|
120
120
|
|
|
121
121
|
function checkAssignableInviteRole(
|
|
@@ -239,7 +239,7 @@ async function issueInvitation(
|
|
|
239
239
|
const token = generateToken();
|
|
240
240
|
await storeInviteToken(ctx.redis, { invitationId, token, ttlSeconds });
|
|
241
241
|
|
|
242
|
-
const locale = resolveHandlerMailLocale(ctx, opts.locale);
|
|
242
|
+
const locale = request.locale ?? resolveHandlerMailLocale(ctx, opts.locale);
|
|
243
243
|
|
|
244
244
|
await dispatchMagicLinkMail(
|
|
245
245
|
ctx.notify,
|
|
@@ -317,6 +317,8 @@ const SystemInviteCreateSchema = z.object({
|
|
|
317
317
|
email: z.email(),
|
|
318
318
|
role: z.string().min(1).max(50),
|
|
319
319
|
globalRoles: z.array(z.enum(GLOBAL_ROLE_OPTIONS)).max(GLOBAL_ROLE_OPTIONS.length).default([]),
|
|
320
|
+
// Preferred over the request locale: provisioning callers know the invitee's language.
|
|
321
|
+
locale: z.string().max(35).refine(isValidLocaleTag, "invalid locale tag").optional(),
|
|
320
322
|
});
|
|
321
323
|
|
|
322
324
|
// System-only sibling of invite-create: the only path that can put global
|
package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts
CHANGED
|
@@ -21,7 +21,7 @@ const SwitchTenantMfaGateSchema = z.object({
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
export type SwitchTenantMfaGateData =
|
|
24
|
-
| { readonly kind: "mfa-gate-clear" }
|
|
24
|
+
| { readonly kind: "mfa-gate-clear"; readonly roles: readonly string[] }
|
|
25
25
|
| NonNullable<Awaited<ReturnType<typeof gateEnforceMfa>>>;
|
|
26
26
|
|
|
27
27
|
// Dispatched by POST /auth/switch-tenant with the system identity. The handler
|
|
@@ -54,7 +54,7 @@ export function createSwitchTenantMfaGateHandler(
|
|
|
54
54
|
);
|
|
55
55
|
const roles = buildSessionRoles(parseRoles(userRow?.roles ?? null), active.membership.roles);
|
|
56
56
|
const mfaGate = await gateEnforceMfa(ctx, opts, userId, tenantId, roles);
|
|
57
|
-
return { isSuccess: true, data: mfaGate ?? { kind: "mfa-gate-clear" } };
|
|
57
|
+
return { isSuccess: true, data: mfaGate ?? { kind: "mfa-gate-clear", roles } };
|
|
58
58
|
},
|
|
59
59
|
});
|
|
60
60
|
}
|
|
@@ -77,24 +77,28 @@ export async function dispatchMagicLinkMail(
|
|
|
77
77
|
// mailT already root-falls-back; appUrl must use the same negotiated tag
|
|
78
78
|
// or language-in-path apps 404 on e.g. /de-AT/activate.
|
|
79
79
|
const urlLocale = resolveMailLocale(locale);
|
|
80
|
+
// Template, expiry formatting and app-name fallback must share one locale,
|
|
81
|
+
// otherwise an English template shows e.g. French "3 heures".
|
|
82
|
+
const templateLocale = authMailTemplateLocale(spec.notificationType, locale);
|
|
80
83
|
const { validFor, validUntil } = tokenMailExpiry(
|
|
81
84
|
{
|
|
82
85
|
expiresAt: params.expiresAt,
|
|
83
86
|
...(params.issuedAt !== undefined && { issuedAt: params.issuedAt }),
|
|
84
87
|
...(params.timeZone !== undefined && { timeZone: params.timeZone }),
|
|
85
88
|
},
|
|
86
|
-
|
|
89
|
+
templateLocale,
|
|
87
90
|
);
|
|
88
91
|
await notify(spec.notificationType, {
|
|
89
92
|
route: { email: params.email },
|
|
90
93
|
data: {
|
|
91
|
-
appName:
|
|
94
|
+
appName:
|
|
95
|
+
params.appName ?? spec.fallbackAppName ?? mailT(templateLocale, "auth.mail.appNameDefault"),
|
|
92
96
|
actionUrl: appendToken(resolveAppUrl(params.appUrl, urlLocale), params.token),
|
|
93
97
|
validFor,
|
|
94
98
|
validUntil,
|
|
95
99
|
...(params.role !== undefined && { role: params.role }),
|
|
96
100
|
},
|
|
97
|
-
locale:
|
|
101
|
+
locale: templateLocale,
|
|
98
102
|
priority: "critical",
|
|
99
103
|
});
|
|
100
104
|
}
|
|
@@ -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: Readonly<Record<string, { readonly en: string }>> = {
|
|
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
|
+
};
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Termination record re-reads the stream after the provider call",
|
|
6
|
+
"detail": "The cancel-at-provider call can take long, so the termination event is appended with a freshly fetched stream version instead of the one read before it."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.365.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Contract mails carry designer labels",
|
|
12
|
+
"detail": "Feature and notify labels for contract-confirmation, contract-termination-receipt and contract-termination-operator-notice name the contract mails in the document designer."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.364.0",
|
|
4
16
|
"type": "fix",
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts
CHANGED
|
@@ -238,7 +238,14 @@ export function createRecordContractTerminationHandler(
|
|
|
238
238
|
effectiveAtIso,
|
|
239
239
|
providerCancel,
|
|
240
240
|
};
|
|
241
|
-
|
|
241
|
+
// Re-fetch after the external call: a webhook appended meanwhile would
|
|
242
|
+
// otherwise fail the append on a stale expectedVersion although the
|
|
243
|
+
// provider already cancelled.
|
|
244
|
+
const streamAfterProviderCall = await ctx.fetchForWriting({
|
|
245
|
+
aggregateId: subscriptionAggregateId(tenantId),
|
|
246
|
+
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
247
|
+
});
|
|
248
|
+
await streamAfterProviderCall.appendOne({
|
|
242
249
|
type: CONTRACT_TERMINATION_REQUESTED_EVENT_QN,
|
|
243
250
|
payload: recorded,
|
|
244
251
|
});
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
type LocalizedString = { readonly en: string };
|
|
5
5
|
|
|
6
6
|
export const BILLING_FOUNDATION_I18N: Readonly<Record<string, LocalizedString>> = {
|
|
7
|
+
"billing-foundation:feature.label": { en: "Billing" },
|
|
8
|
+
"billing-foundation:notify.contract-confirmation.label": { en: "Contract confirmation" },
|
|
9
|
+
"billing-foundation:notify.contract-termination-receipt.label": { en: "Termination receipt" },
|
|
10
|
+
"billing-foundation:notify.contract-termination-operator-notice.label": {
|
|
11
|
+
en: "Termination notice to operator",
|
|
12
|
+
},
|
|
7
13
|
"screen:billing-plans.title": { en: "Billing" },
|
|
8
14
|
"billing-foundation.plans.billingDisabled": {
|
|
9
15
|
en: "Billing is not active yet. Your current plan stays in place.",
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// so it is now a factory parameter.
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
isTierAssignmentSource,
|
|
8
|
+
type TierAssignmentSource,
|
|
7
9
|
TierAssignmentSources,
|
|
8
10
|
TierEngineHandlers,
|
|
9
11
|
TierEngineQueries,
|
|
@@ -67,7 +69,7 @@ type ExistingTierAssignment = {
|
|
|
67
69
|
readonly id: string;
|
|
68
70
|
readonly version: number;
|
|
69
71
|
readonly tier: unknown;
|
|
70
|
-
readonly source:
|
|
72
|
+
readonly source: TierAssignmentSource | null;
|
|
71
73
|
};
|
|
72
74
|
|
|
73
75
|
function findTierAssignment(
|
|
@@ -78,7 +80,13 @@ function findTierAssignment(
|
|
|
78
80
|
if (!row || typeof row["id"] !== "string" || typeof row["version"] !== "number") {
|
|
79
81
|
return undefined;
|
|
80
82
|
}
|
|
81
|
-
|
|
83
|
+
const source = row["source"];
|
|
84
|
+
return {
|
|
85
|
+
id: row["id"],
|
|
86
|
+
version: row["version"],
|
|
87
|
+
tier: row["tier"],
|
|
88
|
+
source: isTierAssignmentSource(source) ? source : null,
|
|
89
|
+
};
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
export function createSubscriptionTierSync<TTier extends string>(
|
|
@@ -280,6 +280,12 @@ export type ReadRollingCapUsageOptions = {
|
|
|
280
280
|
readonly windowDays: number;
|
|
281
281
|
};
|
|
282
282
|
|
|
283
|
+
// A release is written up to a reservation TTL after its increment; once the increment left the
|
|
284
|
+
// window the release must not subtract from other increments still inside it.
|
|
285
|
+
function isBefore(isoInstant: string, cutoff: Temporal.Instant): boolean {
|
|
286
|
+
return Temporal.Instant.compare(Temporal.Instant.from(isoInstant), cutoff) < 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
283
289
|
async function sumRollingEventAmounts(
|
|
284
290
|
db: TenantDb,
|
|
285
291
|
tenantId: TenantId,
|
|
@@ -289,20 +295,26 @@ async function sumRollingEventAmounts(
|
|
|
289
295
|
const aggregateId = rollingCapAggregateId(tenantId, options.capName);
|
|
290
296
|
const cutoff = Temporal.Now.instant().subtract({ hours: options.windowDays * 24 });
|
|
291
297
|
|
|
292
|
-
const rows = await db.selectMany<{ payload: { amount?: number } }>(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
298
|
+
const rows = await db.selectMany<{ payload: { amount?: number; incrementedAt?: string } }>(
|
|
299
|
+
eventsTable,
|
|
300
|
+
{
|
|
301
|
+
tenantId,
|
|
302
|
+
aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
|
|
303
|
+
aggregateId,
|
|
304
|
+
type,
|
|
305
|
+
createdAt: { gte: cutoff },
|
|
306
|
+
},
|
|
307
|
+
);
|
|
299
308
|
|
|
300
309
|
let total = 0;
|
|
301
310
|
for (const row of rows) {
|
|
302
311
|
// @cast-boundary engine-payload — events.payload is jsonb (typed as
|
|
303
312
|
// unknown by drizzle's $type<Record<string,unknown>>); narrowing the
|
|
304
313
|
// shape here mirrors enforceRollingCap's own read-side contract.
|
|
305
|
-
const payload = row["payload"] as { amount?: number };
|
|
314
|
+
const payload = row["payload"] as { amount?: number; incrementedAt?: string };
|
|
315
|
+
if (typeof payload.incrementedAt === "string" && isBefore(payload.incrementedAt, cutoff)) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
306
318
|
if (typeof payload.amount === "number") {
|
|
307
319
|
total += payload.amount;
|
|
308
320
|
}
|
|
@@ -37,6 +37,7 @@ type CapReservationRow = {
|
|
|
37
37
|
readonly periodStartIso: string | null;
|
|
38
38
|
readonly amount: number;
|
|
39
39
|
readonly expiresAt: Temporal.Instant;
|
|
40
|
+
readonly createdAt: Temporal.Instant;
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
const NOTHING_TO_RELEASE: WriteResult = { isSuccess: true, data: {} };
|
|
@@ -136,13 +137,14 @@ function rollingEvent(
|
|
|
136
137
|
amount: number,
|
|
137
138
|
expectedVersion: number,
|
|
138
139
|
type: string,
|
|
140
|
+
extraPayload: { readonly incrementedAt?: string } = {},
|
|
139
141
|
) {
|
|
140
142
|
return {
|
|
141
143
|
aggregateId: rollingCapAggregateId(user.tenantId, capName),
|
|
142
144
|
aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
|
|
143
145
|
expectedVersion,
|
|
144
146
|
type,
|
|
145
|
-
payload: { capName, amount },
|
|
147
|
+
payload: { capName, amount, ...extraPayload },
|
|
146
148
|
metadata: { userId: user.id },
|
|
147
149
|
};
|
|
148
150
|
}
|
|
@@ -213,7 +215,10 @@ async function giveBackRolling(
|
|
|
213
215
|
);
|
|
214
216
|
await appendEventInTenantDb(
|
|
215
217
|
txDb,
|
|
216
|
-
|
|
218
|
+
// The window filter must use the increment's time: a sweep releases up to a TTL later.
|
|
219
|
+
rollingEvent(user, row.capName, row.amount, version, ROLLING_RELEASED_EVENT_QN, {
|
|
220
|
+
incrementedAt: row.createdAt.toString(),
|
|
221
|
+
}),
|
|
217
222
|
);
|
|
218
223
|
return NOTHING_TO_RELEASE;
|
|
219
224
|
}
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "Handlers wrapped with withCapEnforcement or withRollingCapEnforcement are rejected when called through a nested ctx.write",
|
|
6
|
+
"detail": "Since the wrappers reserve through reserveBeforeTransaction, the dispatcher rejects a nested ctx.write, ctx.writeAs or handler-to-handler call of such a handler with an internal error (\"has a pre-transaction phase ... cannot run as a nested write\") instead of running it. Before the reservation lifecycle change the wrapped handler ran normally when called nested.",
|
|
7
|
+
"migration": "Dispatch rolling- and calendar-capped handlers only as top-level commands (HTTP write or batch). A handler that calls one through ctx.write declares the cap on itself instead, or the call moves out of the handler into a client or job that dispatches a top-level command."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.365.0",
|
|
11
|
+
"type": "fix",
|
|
12
|
+
"title": "Rolling cap release remembers the original booking time",
|
|
13
|
+
"detail": "rolling-released events now carry the incrementedAt of the booking they return, so readRollingCapUsage no longer subtracts a release whose increment already left the rolling window."
|
|
14
|
+
},
|
|
2
15
|
{
|
|
3
16
|
"version": "0.364.0",
|
|
4
17
|
"type": "breaking",
|
|
@@ -40,6 +40,8 @@ export const rollingIncrementedSchema = incrementRollingSchema;
|
|
|
40
40
|
/** Payload of the event that gives a reserved rolling amount back. */
|
|
41
41
|
export const rollingReleasedSchema = incrementRollingSchema.extend({
|
|
42
42
|
amount: z.number().int().positive(),
|
|
43
|
+
/** When the released increment was booked; absent on events written before it was recorded. */
|
|
44
|
+
incrementedAt: z.string().optional(),
|
|
43
45
|
});
|
|
44
46
|
|
|
45
47
|
// Rolling-Increment-Handler — append-only. Race-frei: zwei parallele
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Cap overview lists no destroyed tenants",
|
|
6
|
+
"detail": "tenant-options and tenant-caps-list now filter destroyed tenants and keep reading tenant names through the PII-aware path."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.338.0",
|
|
4
10
|
"type": "breaking",
|
|
@@ -176,7 +176,8 @@ export function createTenantCapsListQuery(caps: readonly CapSpec[], listCaps: re
|
|
|
176
176
|
);
|
|
177
177
|
|
|
178
178
|
const tenants = await decryptTenantNames(
|
|
179
|
-
|
|
179
|
+
// Destroyed tenants decrypt to the erased sentinel, which a picker or list would show verbatim.
|
|
180
|
+
await selectMany<TenantRow>(db, tenantTable, { destroyedAt: null }),
|
|
180
181
|
"cap-overview:tenant-caps:list",
|
|
181
182
|
);
|
|
182
183
|
const assignments = await selectMany<TierAssignmentRow>(db, tierAssignmentTable, {});
|
|
@@ -23,7 +23,8 @@ export const tenantOptionsQuery = defineQueryHandler({
|
|
|
23
23
|
"cap-overview:tenant-options — SystemAdmin dashboard tenant-filter options",
|
|
24
24
|
);
|
|
25
25
|
const tenants = await decryptTenantNames(
|
|
26
|
-
|
|
26
|
+
// Destroyed tenants decrypt to the erased sentinel, which the picker would show verbatim.
|
|
27
|
+
await selectMany<TenantRow>(db, tenantTable, { destroyedAt: null }),
|
|
27
28
|
"cap-overview:tenant-options",
|
|
28
29
|
);
|
|
29
30
|
const sorted = [...tenants].sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export type DiscordChannelOptions = ChatWebhookChannelOptions;
|
|
10
10
|
|
|
11
|
+
const DISCORD_WEBHOOK_PREFIX = "channel-discord:webhooks.";
|
|
12
|
+
|
|
11
13
|
export const DISCORD_SECRET_KEYS = {
|
|
12
|
-
webhookPrefix:
|
|
13
|
-
webhookKeyFor: (connection: string) =>
|
|
14
|
+
webhookPrefix: DISCORD_WEBHOOK_PREFIX,
|
|
15
|
+
webhookKeyFor: (connection: string) => `${DISCORD_WEBHOOK_PREFIX}${connection}`,
|
|
14
16
|
} as const;
|
|
15
17
|
|
|
16
18
|
export const DISCORD_DEFAULT_ALLOWED_HOSTS = ["discord.com", "discordapp.com"] as const;
|